首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 動態修改螢幕解析度:P/Invoke 呼叫 Win32 API 完整實作與 DEVMODE 結構對應

     編輯:關於C語言

前言:.NET 無托管 API,必須跨界呼叫

.NET Framework / Core 沒有直接修改顯示設定的托管 API。必須透過 P/Invoke 平台調用 user32.dll 的 EnumDisplaySettings 與 ChangeDisplaySettings,核心在於正確定義 DEVMODE 結構對應記憶體佈局。

步驟一:精確定義 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 會失敗。

步驟二:P/Invoke 簽名與常數定義

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);
    }
}

步驟四:取得支援的解析度清單 (EnumDisplaySettings 迴圈)

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("切換成功");
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved