文号
827687
阅读
11740
回复
2
收藏
1
Windows Forms/GDI+、Win32/GDI中的高DPI编程
2016/11/15
IP:四川

由于历史原因,GDI和GDI+中高DPI编程都是系统级DPI,没有逐显示器DPI支持,由于后者的复杂性,以及不支持逐显示器DPI的Windows 7尚未完全淘汰,因此此处不讨论逐显示器DPI。

打开系统级DPI支持

如果你的程序是WPF程序,那么已经打开。如果是Windows Forms程序,打开高DPI还需要额外的操作。更改Program.cs:

using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace HighGdiDpi
{
    static class Program
    {
        [DllImport("kernel32.dll")]
        private static extern IntPtr GetModuleHandle(string modname);
        [DllImport("kernel32.dll")]
        private static extern IntPtr GetProcAddress(IntPtr modhandle, string procname);
        private delegate bool DelSetProcessDPIAware();

        /// <summary>
        /// 应用程序的主入口点。
        /// </summary>
        [STAThread]
        static void Main()
        {
            IntPtr modhandle = GetModuleHandle("user32");
            if (modhandle != IntPtr.Zero)
            {
                IntPtr procaddress = GetProcAddress(modhandle, "SetProcessDPIAware");
                if (procaddress != IntPtr.Zero)
                {
                    DelSetProcessDPIAware SetProcessDPIAware = (DelSetProcessDPIAware)Marshal.GetDelegateForFunctionPointer(procaddress, typeof(DelSetProcessDPIAware));
                    SetProcessDPIAware();
                }
            }

            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}

同时,窗体的AutoScaleMode属性应该设置为Dpi,默认为Font,这个值现在已经过时了。

this.AutoScaleMode = AutoScaleMode.Dpi;

增强的Windows Forms控件缩放

如果用户安装了.NET Framework 4.5.2或更高版本(Windows 10已自带了更高版本),那么用户可以获得增强的Windows Forms控件缩放。不过这需要添加App.config设置。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appsettings>
    <add key="EnableWindowsFormsHighDpiAutoResizing" value="true">
  </add></appsettings>
</configuration>

需要注意的是,只有.NET Framework 4.0以上版本才能自动使用更高版本,如果2.0/3.0/3.5版本也想拥有这个特性,需要设置supportedRuntime和useLegacyV2RuntimeActivationPolicy。

<?xml version="1.0"?>
<configuration>
  <startup uselegacyv2runtimeactivationpolicy="true">
    <supportedruntime version="v4.0.30319">
    <supportedruntime version="v2.0.50727">
  </supportedruntime></supportedruntime></startup>
  <appsettings>
    <add key="EnableWindowsFormsHighDpiAutoResizing" value="true">
  </add></appsettings>
</configuration>

GDI+

GDI+可以调节单位。其中Pixel是像素单位,Inch是GDI+使用的分辨率无关单位,我们用DIP(设备无关像素)表示96 DPI下的像素,Inch与DIP之间的换算关系为:

  • Inch = DIP / 96.0f

同时,Pixel与DIP之间的换算关系为:

  • Pixel = DIP * 系统DPI值 / 96.0f

设置GDI+单位为Inch的方法很简单,直接设置PageUnit属性就行了:

g.PageUnit = GraphicsUnit.Inch;

获取DPI值使用g.DpiX和g.DpiY属性,它们一般是相等的。

下面是一个简单的使用分辨率无关单位Inch绘图的程序。

WindowsFormsGdiPlusLowDpi.png

WindowsFormsGdiPlusHighDpi.png

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace HighGdiDpi
{
    public partial class Form1 : Form
    {
        Timer timer = new Timer();

        public Form1()
        {
            InitializeComponent();
            this.AutoScaleMode = AutoScaleMode.Dpi;

            this.Paint += Form1_Paint;
            timer.Tick += timer_Tick;
            timer.Interval = 50;
            timer.Enabled = true;
        }

        // 需要重绘时绘图
        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            buffered_draw(e.Graphics);
        }

        // 实时绘图(可选)
        private void timer_Tick(object sender, EventArgs e)
        {
            buffered_draw(this.CreateGraphics());
        }

        // 实现缓冲绘图
        private void buffered_draw(Graphics g)
        {
            BufferedGraphicsContext ctx = BufferedGraphicsManager.Current;
            using (BufferedGraphics bufg = ctx.Allocate(g, ClientRectangle))
            using (Graphics g2 = bufg.Graphics)
            {
                draw(g2);
                bufg.Render();
            }
        }

        // 最终调用的绘图函数
        public void draw(Graphics g)
        {
            // 清空画面
            g.Clear(Color.White);

            // 绘图
            g.PageUnit = GraphicsUnit.Inch; // 分辨率无关单位
            Pen bluepen = new Pen(Brushes.Blue, 1.0f / 96.0f);
            g.DrawLine(bluepen, new PointF(10 / 96.0f, 10 / 96.0f), new PointF(100 / 96.0f, 100 / 96.0f));
            g.DrawString("我是来自GDI+的问候", SystemFonts.DefaultFont, Brushes.Red, new PointF(10 / 96.0f, 10 / 96.0f));

            // 输出
            g.Flush();
        }
    }
}

动态调整DPI的GDI+绘图

如果需要动态调整GDI+绘图的DPI,就不能使用BufferedGraphics。必须使用Bitmap建立一个画布,然后SetResolution设置动态DPI,绘制完成以后再g.DrawImageUnscaledAndClipped绘制出来。

private void buffered_dynamicdpi_draw(Graphics g, int clientwidth, int clientheight, float dpix, float dpiy)
{
    using (Bitmap backbmp = new Bitmap(clientwidth, clientheight))
    {
        backbmp.SetResolution(dpix, dpiy);
        using (Graphics g2 = Graphics.FromImage(backbmp))
        {
            draw(g2);
            g2.Flush();
        }
        g.DrawImageUnscaledAndClipped(backbmp, new Rectangle(0, 0, clientwidth, clientheight));
    } 
}
[ 修改于10年1个月前 - 2016/11/16 16:51:20 ]
acmilan作者
10年1个月前修改于 10年0个月前IP:四川
827698
1楼

Win32/GDI编程中的DPI

打开高DPI的方法

与.NET的方法相同,所不同的是Win32编程中不需要手动引入GetModuleHandle和GetProcAddress两个WinAPI。

HMODULE hUser32 = GetModuleHandleA("user32");
if (hUser32)
{
	FARPROC pSetProcessDpiAware = GetProcAddress(hUser32, "SetProcessDPIAware");
	if (pSetProcessDpiAware)
	{
		pSetProcessDpiAware();
	}
}

获取系统DPI的方法

和.NET不同的是,Win32并不会给我们自动缩放,大部分情况下我们需要手动缩放。这时就需要

首先通过GetDC(NULL)获取屏幕的HDC,然后GetDeviceCaps(hscreendc, LOGPIXELSX)获取X方向的DPI,使用GetDeviceCaps(hscreendc, LOGPIXELSY)获取Y方向的DPI,一般来说,它们是相等的,获取完毕,务必使用ReleaseDC(NULL, hscreendc)释放屏幕的HDC。

int dpix = 96;
int dpiy = 96;

HDC hscreendc = GetDC(NULL);
if (hscreendc)
{
	dpix = GetDeviceCaps(hscreendc, LOGPIXELSX);
	dpiy = GetDeviceCaps(hscreendc, LOGPIXELSY);
	ReleaseDC(NULL, hscreendc);
}

使用MulDiv进行手工缩放

一般来说,我们可以使用MulDiv进行缩放:

RECT rc = { 0, 0, 320, 240 };
rc.left = MulDiv(rc.left, dpix, 96);
rc.right = MulDiv(rc.right, dpix, 96);
rc.top = MulDiv(rc.top, dpiy, 96);
rc.bottom = MulDiv(rc.bottom, dpiy, 96);

MulDiv只适用于整数,对于浮点数来说,还是要使用x * dpix / 96.0f和y * dpiy / 96.0f。

使用AdjustWindowRect调整窗口大小

CreateWindow要求窗口大小,但是我们通常期望设置客户区大小,这时候就要使用AdjustWindowRect了。注意AdjustWindowRect不能预知菜单栏有没有被折行,因此如果菜单项很多的话,不一定能计算出准确的窗口大小。

以下是一个经过DPI缩放和AdjustWindowRect计算的后调用CreateWindows的程序:

AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, TRUE);

hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT, CW_USEDEFAULT, rc.right - rc.left, rc.bottom - rc.top, NULL, NULL, hInstance, NULL);

关于GDI+

与WinForms第二种方法基本相同,不同的是,Win32版本的GDI+没有DrawImageUnscaled,需要使用g.DrawImage(&bmp, 0, 0, bmp.GetWidth(), bmp.GetHeight());这种用法。

这里有个坑,获得HDC之后必须第一时间创建Graphics对象,不能GDI和GDI+混用,不然GetLastStatus()会返回3,即OutOfMemory,无法使用GetDpiX()和GetDpiY()函数。

关于GDI

由于GDI年代比较久远,不提供浮点数计算,因此不能想当然的使用MM_LOENGLISH或MM_HIENGLISH。比较科学的方法是使用MM_TWIPS。它与DIP之间的换算关系如下:

TWIPS.x = DIP.x * 20

TWIPS.y = DIP.y * -20

由于GDI对除MM_TEXT以外的单位都使用Y轴向上的坐标系,因此比较蛋疼的是,y需要取负才能表示显示在屏幕上的点。

case WM_PAINT:
	{
		HDC hdc = BeginPaint(hWnd, &ps);
		SetMapMode(hdc, MM_TWIPS);
		MoveToEx(hdc, 10 * 20, 10 * -20, NULL);
		LineTo(hdc, 100 * 20, 100 * -20);
		EndPaint(hWnd, &ps);
		return 0;
	}

此外,TWIPS与Pixel的转换不需要手工进行,只需要使用LPtoDP和DPtoLP即可。

RECT rc = { 10 * 20, 10 * -20, 300 * 20, 200 * -20 };
LPtoDP(hdc, (LPPOINT)&rc, 2);

运行效果:

Win32LowDpi.png

Win32HighDpi.png

引用
评论
acmilan作者
10年1个月前修改于 10年0个月前IP:四川
827712
2楼

经常GDI与GDI+混用,经过调试后发现,先调用GDI再创建Graphics会导致GDI+创建Graphics返回3(OutOfMemory),某些调用如GetDpiX()和GetDpiY()会直接失败,所以实际上是不能混用的。

Direct2D、DirectWrite等Windows 7新加入的技术比GDI+更可靠效率也更高,但是却不支持Windows XP等老系统,sad。

引用
评论
笔记
{{note.content}}
{{n.user.username}}
{{fromNow(n.toc)}} {{n.status === noteStatus.disabled ? "已屏蔽" : ""}} {{n.status === noteStatus.unknown ? "正在审核" : ""}} {{n.status === noteStatus.deleted ? '已删除' : ''}}
  • 编辑
  • 删除
  • {{n.status === 'disabled' ? "解除屏蔽" : "屏蔽" }}
我也是有底线的