.NET Framework / Core 沒有直接修改顯示設定的托管 API。必須透過 P/Invoke 平台調用 user32.dll 的 EnumDisplaySettings 與 ChangeDisplaySettings,核心在於正確定義 DEVMODE 結構對應記憶體佈局。
對應 Win32 DEVMODEA,使用 StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi) 確保欄位順序與大小一致。
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct DEVMODE
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string dmDeviceName;
public short dmSpecVersion;
public short dmDriverVersion;
public short dmSize;
public short dmDriverExtra;
public int dmFields;
public int dmPositionX;
public int dmPositionY;
public int dmDisplayOrientation;
public int dmDisplayFixedOutput;
public short dmColor;
public short dmDuplex;
public short dmYResolution;
public short dmTTOption;
public short dmCollate;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string dmFormName;
public short dmLogPixels;
public short dmBitsPerPel;
public int dmPelsWidth; // 寬度 (解析度)
public int dmPelsHeight; // 高度 (解析度)
public int dmDisplayFlags;
public int dmDisplayFrequency; // 重新整理率
public int dmICMMethod;
public int dmICMIntent;
public int dmMediaType;
public int dmDitherType;
public int dmReserved1;
public int dmReserved2;
public int dmPanningWidth;
public int dmPanningHeight;
}
重點:dmSize 必須在呼叫前設為 (short)Marshal.SizeOf<DEVMODE>(),否則 API 會失敗。
public static class NativeMethods
{
[DllImport("user32.dll", CharSet = CharSet.Ansi)]
public static extern bool EnumDisplaySettings(string lpszDeviceName, int iModeNum, ref DEVMODE lpDevMode);
[DllImport("user32.dll", CharSet = CharSet.Ansi)]
public static extern int ChangeDisplaySettings(ref DEVMODE lpDevMode, int dwFlags);
// 模式編號
public const int ENUM_CURRENT_SETTINGS = -1;
public const int ENUM_REGISTRY_SETTINGS = -2;
// ChangeDisplaySettings flags
public const int CDS_NONE = 0;
public const int CDS_UPDATEREGISTRY = 0x01; // 寫入登錄永久生效
public const int CDS_TEST = 0x02; // 僅測試不套用
public const int CDS_FULLSCREEN = 0x04; // 全螢幕模式
public const int CDS_GLOBAL = 0x08; // 全域 (需管理員)
public const int CDS_SET_PRIMARY = 0x10;
public const int CDS_VIDEOPARAMETERS = 0x20;
// 回傳碼
public const int DISP_CHANGE_SUCCESSFUL = 0;
public const int DISP_CHANGE_RESTART = 1;
public const int DISP_CHANGE_FAILED = -1;
public const int DISP_CHANGE_BADMODE = -2;
public const int DISP_CHANGE_NOTUPDATED = -3;
public const int DISP_CHANGE_BADFLAGS = -4;
public const int DISP_CHANGE_BADPARAM = -5;
// 螢幕旋轉方向
public const int DMDO_DEFAULT = 0;
public const int DMDO_90 = 1;
public const int DMDO_180 = 2;
public const int DMDO_270 = 3;
}
public static class DisplayHelper
{
/// 變更解析度,可選是否永久寫入登錄
public static bool ChangeResolution(int width, int height, bool permanent = true, int frequency = 0)
{
DEVMODE dm = new DEVMODE();
dm.dmDeviceName = new string('\0', 32);
dm.dmFormName = new string('\0', 32);
dm.dmSize = (short)Marshal.SizeOf();
// 1. 取得當前設定
if (!NativeMethods.EnumDisplaySettings(null, NativeMethods.ENUM_CURRENT_SETTINGS, ref dm))
{
MessageBox.Show("無法讀取當前顯示設定");
return false;
}
// 2. 修改目標欄位
dm.dmPelsWidth = width;
dm.dmPelsHeight = height;
if (frequency > 0)
{
dm.dmDisplayFrequency = frequency;
dm.dmFields |= 0x00400000; // DM_DISPLAYFREQUENCY
}
dm.dmFields |= 0x180000; // DM_PELSWIDTH | DM_PELSHEIGHT
// 3. 僅測試 (CDS_TEST)
int testRet = NativeMethods.ChangeDisplaySettings(ref dm, NativeMethods.CDS_TEST);
if (testRet != NativeMethods.DISP_CHANGE_SUCCESSFUL)
{
ShowError(testRet);
return false;
}
// 4. 正式套用
int flags = permanent ? NativeMethods.CDS_UPDATEREGISTRY : NativeMethods.CDS_NONE;
int ret = NativeMethods.ChangeDisplaySettings(ref dm, flags);
if (ret == NativeMethods.DISP_CHANGE_SUCCESSFUL) return true;
ShowError(ret);
return false;
}
private static void ShowError(int code)
{
string msg = code switch
{
NativeMethods.DISP_CHANGE_RESTART => "需重新啟動電腦才能生效",
NativeMethods.DISP_CHANGE_BADMODE => "顯示卡不支援此模式",
NativeMethods.DISP_CHANGE_FAILED => "顯示驅動程式變更失敗",
_ => $"未知錯誤代碼: {code}"
};
MessageBox.Show(msg, "解析度變更失敗", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
public static List<(int W, int H, int Freq)> GetSupportedModes()
{
var list = new List<(int, int, int)>();
DEVMODE dm = new DEVMODE();
dm.dmSize = (short)Marshal.SizeOf();
for (int i = 0; ; i++)
{
if (!NativeMethods.EnumDisplaySettings(null, i, ref dm)) break;
list.Add((dm.dmPelsWidth, dm.dmPelsHeight, dm.dmDisplayFrequency));
}
return list.Distinct().OrderBy(x => x.W).ThenBy(x => x.H).ToList();
}
| 主題 | 處理方式 |
|---|---|
| 多螢幕 | EnumDisplaySettings 第一參數傳裝置名 (如 \\.\DISPLAY1),可用 Screen.AllScreens 取得 DeviceName。 |
| 螢幕旋轉 | 設定 dmDisplayOrientation = DMDO_90 等,dmFields 加入 0x00800000 (DM_DISPLAYORIENTATION)。 |
| 需管理員權限 | CDS_GLOBAL / CDS_UPDATEREGISTRY 寫入 HKLM 需提權;建議應用程式清單設 requireAdministrator。 |
| 高 DPI 感知 | 專案屬性勾選「DPI 感知」或 SetProcessDpiAwareness,避免座標虛擬化干擾。 |
// 切換到 1920x1080 @60Hz,永久生效
bool ok = DisplayHelper.ChangeResolution(1920, 1080, permanent: true, frequency: 60);
if (ok) Console.WriteLine("切換成功");