如何使用Delphi VCL修改副显示器的显示方向
Delphi VCL 副显示器显示方向修改方案
副屏修改不生效、System error 5、DLL调用异常三类问题的核心诱因集中在三点:
- 调用
ChangeDisplaySettingsEx时第一个参数传空,默认只会作用于主显示器,无法触发副屏配置变更 - DEVMODE结构定义与当前系统/编译位宽不匹配,
dmSize字段赋值错误,触发内存访问异常 - 未正确设置
dmFields标识位导致系统忽略方向参数,或调用场景不符合权限要求触发拒绝访问
前置说明
该方案完全基于Win32原生API实现,不需要引入Firemonkey任何单元,兼容Delphi 7到Delphi 12全版本,支持32位、64位编译。核心依赖的API都在VCL自带的Windows单元中,无需额外引入第三方库。
必要常量与结构定义
Delphi自带的DEVMODE结构在部分旧版本中缺少显示方向相关字段,直接使用会出现参数错位,手动定义适配版本即可:
const // 显示方向常量 DMDO_DEFAULT = 0; // 0度 标准横向 DMDO_90 = 1; // 90度 纵向 DMDO_180 = 2; // 180度 横向翻转 DMDO_270 = 3; // 270度 纵向翻转 // DEVMODE字段有效标识 DM_DISPLAYORIENTATION = $00000080; DM_PELSHEIGHT = $00000010; DM_PELSWIDTH = $00000008; // 配置变更标识 CDS_UPDATEREGISTRY = $00000001; DISP_CHANGE_SUCCESSFUL = 0; type // 内存对齐的适配版DEVMODE结构,避免不同版本定义差异 TDeviceMode = packed record dmDeviceName: array[0..CCHDEVICENAME - 1] of WideChar; dmSpecVersion: Word; dmDriverVersion: Word; dmSize: Word; dmDriverExtra: Word; dmFields: DWORD; dmOrientation: SHORT; dmPaperSize: SHORT; dmPaperLength: SHORT; dmPaperWidth: SHORT; dmScale: SHORT; dmCopies: SHORT; dmDefaultSource: SHORT; dmPrintQuality: SHORT; dmPosition: POINTL; dmDisplayOrientation: DWORD; dmDisplayFixedOutput: DWORD; dmColor: SHORT; dmDuplex: SHORT; dmYResolution: SHORT; dmTTOption: SHORT; dmCollate: SHORT; dmFormName: array[0..CCHFORMNAME - 1] of WideChar; dmLogPixels: Word; dmBitsPerPel: DWORD; dmPelsWidth: DWORD; dmPelsHeight: DWORD; dmDisplayFlags: DWORD; dmDisplayFrequency: DWORD; dmICMMethod: DWORD; dmICMIntent: DWORD; dmMediaType: DWORD; dmDitherType: DWORD; dmReserved1: DWORD; dmReserved2: DWORD; dmPanningWidth: DWORD; dmPanningHeight: DWORD; end;
实现逻辑
- 调用
EnumDisplayDevices枚举所有显示设备,通过DISPLAY_DEVICE_PRIMARY_DEVICE标识区分主副屏,记录目标副屏的设备名 - 调用
EnumDisplaySettings拉取目标副屏的当前生效配置,初始化DEVMODE结构,必须将dmSize赋值为结构实际内存大小 - 根据目标旋转角度调整宽高参数:旋转90度、270度时需要交换屏幕宽高值,避免画面拉伸
- 设置
dmDisplayOrientation为目标角度,同时给dmFields加上对应有效位标记,告知系统哪些配置项需要更新 - 调用
ChangeDisplaySettingsEx传入目标副屏的设备名提交配置变更,校验返回值,失败时自动回滚
完整实现代码
uses Windows, SysUtils, Forms; // 枚举指定索引的显示器,返回设备名与是否为主屏标识 function GetDisplayDevice(DeviceIndex: Integer; out DeviceName: string; out bPrimary: Boolean): Boolean; var DispDev: TDisplayDevice; begin Result := False; ZeroMemory(@DispDev, SizeOf(TDisplayDevice)); DispDev.cb := SizeOf(TDisplayDevice); if EnumDisplayDevices(nil, DeviceIndex, DispDev, 0) then begin bPrimary := (DispDev.StateFlags and DISPLAY_DEVICE_PRIMARY_DEVICE) <> 0; DeviceName := DispDev.DeviceName; Result := True; end; end; // 修改指定显示器的显示方向 // RotateAngle取值: 0=0度 1=90度 2=180度 3=270度 function SetDisplayRotation(const DeviceName: string; RotateAngle: DWORD): Boolean; var DevMode: TDeviceMode; Ret: Longint; nWidth, nHeight: DWORD; begin Result := False; if not (RotateAngle in [DMDO_DEFAULT, DMDO_90, DMDO_180, DMDO_270]) then Exit; ZeroMemory(@DevMode, SizeOf(TDeviceMode)); DevMode.dmSize := SizeOf(TDeviceMode); // 拉取目标显示器当前配置 if not EnumDisplaySettings(PChar(DeviceName), ENUM_CURRENT_SETTINGS, DevMode) then Exit; // 90/270度旋转需要交换宽高,避免画面拉伸 nWidth := DevMode.dmPelsWidth; nHeight := DevMode.dmPelsHeight; if (RotateAngle = DMDO_90) or (RotateAngle = DMDO_270) then begin DevMode.dmPelsWidth := nHeight; DevMode.dmPelsHeight := nWidth; end; DevMode.dmDisplayOrientation := RotateAngle; // 标记需要更新的配置项 DevMode.dmFields := DM_DISPLAYORIENTATION or DM_PELSWIDTH or DM_PELSHEIGHT; // 传入设备名才能修改对应副屏,传空默认只改主屏 Ret := ChangeDisplaySettingsEx(PChar(DeviceName), DevMode, 0, CDS_UPDATEREGISTRY, nil); if Ret = DISP_CHANGE_SUCCESSFUL then Result := True else // 配置失败自动回滚 ChangeDisplaySettingsEx(PChar(DeviceName), DevMode, 0, 0, nil); end; // 调用示例:将第一个识别到的副屏设置为90度纵向 procedure TForm1.Button1Click(Sender: TObject); var i: Integer; DevName: string; bIsPrimary: Boolean; begin i := 0; while GetDisplayDevice(i, DevName, bIsPrimary) do begin if not bIsPrimary then begin if SetDisplayRotation(DevName, DMDO_90) then ShowMessage('副显示器方向修改成功') else ShowMessage('修改失败,错误码:' + IntToStr(GetLastError)); Break; end; Inc(i); end; end;
异常排查
- System error 5 访问被拒绝:该错误不需要提权管理员,是调用场景问题:不要在系统锁屏、UAC安全桌面、非控制台远程会话下调用,确保程序运行在当前登录用户的交互桌面即可
- DLL调用异常/内存报错:检查DEVMODE结构是否用
packed修饰,dmSize是否用SizeOf()动态计算,不要硬编码固定数值,避免32/64位编译时结构大小差异导致内存越界 - 配置不生效/只有主屏变化:检查
ChangeDisplaySettingsEx第一个参数是否传入目标副屏的完整设备名,不能传空;检查dmFields是否包含DM_DISPLAYORIENTATION标记,未标记时系统会直接忽略方向参数 - 旋转后画面拉伸/显示不全:90度、270度旋转时必须交换宽高值,180度旋转不需要交换宽高
内容的提问来源于stack exchange,提问作者Harry
相关产品推荐
相关产品推荐

