自定义 Font/ColorDialog 等常用对话框样式

本文最后更新于:1 天前

前言

之前的文章 (后文也用 “之前” 来表示这篇文章) 中,我们学会了如何通过 HookProc 使 Font/ColorDialog 具有深色主题。可是其中 FontDialog 的效果非常令人不爽,如果按照常规思路处理自绘来强行应用深色主题的话肯定是行不通的,几乎无解。

然而今天我们就可以来解决这个问题,虽然效果并非 100%,但好歹有个 90%,总之比先前更好。

剖析 FontDialog

首先我们得知道在 Win32/WinForms 中,弹出 Font/ColorDialog 的最最核心的代码是什么。

这里以 WinForms 的 FontDialog 为例,其他采用相同原理的对话框可以以此类推。

想想我们是怎么弹出 FontDialog 的?是不是:

1
new FontDialog().ShowDialog();

这是最简洁的方法,于是我们可以看看 ShowDialog() 的实现。

我们将鼠标悬停在 ShowDialog() 方法上:

得知这个方法位于 CommonDialog 类中而不是 FontDialog。

打开 GitHub 仓库 相关页面,发现这个方法调用了 ShowDialog(null);

为什么不直接在 Visual Studio 里查看定义或者使用反编译工具来获取源码?
因为 C# 编译后再反编译出来会与编译前的代码有些许差别,所以打开官方仓库查看源代码更有意义。

我们来分析一下这个方法的实现

可见在这里调用了 RunDialog 方法,接着我们得知这是个抽象方法,由派生类即 FontDialog 自行实现,所以我们需要看看 FontDialog 是怎样实现的:

这里初始化了一个 CHOOSEFONT 结构,用屁股想都知道是配置 FontDialog 的,用于决定一些参数。那么这个结构体传到了哪里呢?

可见传入了一个名叫 ChooseFont 的 API 里,官方文档在这

毫无疑问,这就是最最核心的代码。对话框的弹出完全由 ChooseFont 函数实现,WinForms 只是封装了这个函数并做了一些擦边的事。

初识 CHOOSEFONT

那么传入的那个 cf 结构体又包含了什么定义呢?打开官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct tagCHOOSEFONTW {
DWORD lStructSize;
HWND hwndOwner;
HDC hDC;
LPLOGFONTW lpLogFont;
INT iPointSize;
DWORD Flags;
COLORREF rgbColors;
LPARAM lCustData;
LPCFHOOKPROC lpfnHook;
LPCWSTR lpTemplateName;
HINSTANCE hInstance;
LPWSTR lpszStyle;
WORD nFontType;
WORD ___MISSING_ALIGNMENT__;
INT nSizeMin;
INT nSizeMax;
} CHOOSEFONTW;

有关这个结构体的字段,我们只需要知道以下几个重要的就行了

  1. lStructSize
    该结构体在内存中的大小。Win32 的很多结构体都包含了类似的字段,比如其他常见的 cbSize、dwSize 之类的。该种字段的作用是告诉系统该结构体的大小,这能让系统来判断哪些字段可用,从而实现兼容并避免越界访问。人话就是表示是什么样的结构,或者说该结构的哪个版本。

  2. hwndOwner
    所有者的窗口句柄,也就是指定 FontDialog 的父窗体 (窗口) 是谁。

  3. lpLogFont
    初始以及用户选择的字体。

  4. Flags
    FontDialog 的参数,比如是否显示颜色选项、是否允许垂直字体等。详见 成员: Flags。常用的有:

    • CF_ENABLEHOOK: 允许 HookProc。当包含此 Flag 时,FontDialog 将退回到旧版样式,否则为新版样式 (字体变成了 Segoe UI,以及包含一个跳转到系统字体设置界面的超链接,感兴趣朋友们的可以试试)
    • CF_ENABLETEMPLATE: 允许自定义对话框模板 (本文重点,后面会讲)
    • CF_FORCEFONTEXIST: 不允许用户输入不存在的字体的名称
    • CF_INITTOLOGFONTSTRUCT:允许 FontDialog 在打开后自动选中指定的字体
    • CF_LIMITSIZE: 设置允许用户选择字体的大小范围
    • CF_NOVERTFONTS: 不允许选择垂直样式的字体 (名称开头带有 @ 的字体)
    • CF_SCRIPTSONLY: 只能选择标准字体,比如那些包含或只有符号的,即不会被渲染为预期字符的那种字体就不会在列表中
    • CF_TTONLY: 只能选择 TrueType 字体 (WinForms 就只支持 TrueType)
  5. lpfnHook
    HookProc 函数指针,也就是 C# 中的委托。要求 Flags 里包含 CF_ENABLEHOOK

  6. lpTemplateNamehInstance
    指定 FontDialog 使用的对话框模板,前者表示模板的名称,后者表示模板所在的模块 (dll 或 exe 等) 的句柄。要求 Flags 里包含 CF_ENABLETEMPLATE

  7. nSizeMinnSizeMax
    指定用户可选择字体的大小范围,单位为 pt。要求 Flags 里包含 CF_LIMITSIZE

初识对话框模板

如果你曾做过 Win32 窗口开发,你可能清楚对话框模板是什么东西。没有开发过也没事,Resource Hacker 玩过吧,那你应该偶然打开过某 exe/dll 的资源中的 Dialog,然后你就可以在这里看到目标软件的资源文件里包含的对话框的样式:

看到后面的代码了吗?那些就是对话框模板,只不过这里看到的与实际的还是有点差别,但基本一致。

如果你做过 WinForms 开发,那你就可以简单将其理解为窗体设计器生成的代码,只不过对话框模板只包含了最基本的控件类型、文本、样式、位置、大小等信息,更小巧。

还记得为什么我们对 FontDialog 的那四个 ComboBox 的深色主题无从下手吗?

因为在 FontDialog 的模板 (Windows SDK\um\Font.Dlg) 中,分别是这样定义的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
COMBOBOX        cmb1, 7, 16, 98, 76,
CBS_SIMPLE | CBS_AUTOHSCROLL | CBS_DISABLENOSCROLL |
CBS_SORT | WS_VSCROLL | WS_TABSTOP | CBS_HASSTRINGS |
CBS_OWNERDRAWFIXED

COMBOBOX cmb2, 110, 16, 74, 76,
CBS_SIMPLE | CBS_AUTOHSCROLL | CBS_DISABLENOSCROLL |
WS_VSCROLL | WS_TABSTOP | CBS_HASSTRINGS |
CBS_OWNERDRAWFIXED

COMBOBOX cmb3, 190, 16, 36, 76,
CBS_SIMPLE | CBS_AUTOHSCROLL | CBS_DISABLENOSCROLL |
CBS_SORT | WS_VSCROLL | WS_TABSTOP | CBS_HASSTRINGS |
CBS_OWNERDRAWFIXED

COMBOBOX cmb5, 110, 157, 116, 30, CBS_DROPDOWNLIST |
CBS_OWNERDRAWFIXED | CBS_AUTOHSCROLL | CBS_HASSTRINGS |
WS_BORDER | WS_VSCROLL | WS_TABSTOP

没错,都启用了 CBS_OWNERDRAWFIXED,也就是自绘。

当控件启用自绘后,不再使用系统默认的行为,(部分) 样式就几乎完全由开发者决定。而我们无法得知对方完整的自绘流程,虽然肯定有办法来达到类似的效果,但是工作量太大,也不推荐这样。

你可能想到了,那我们在 HookProc 里取消各 ComboBox 的 CBS_OWNERDRAWFIXED 样式不就完了?

还真不行,就算可以,也不会达到预期,甚至会出现副作用。

这时对话框模板就发挥很大的作用了。因为控件样式只能在创建时指定,所以我们只需要将官方的 FontDialog 模板搬到我们的项目中,然后直接删除相关样式不就行了?

这真是一个很甜菜的想法,当然不是我想到的,是 ozone10DarkModeSubclass.h 中提到的,给予了我启发。

那我们就直接开干。这里就只说方法不附截图了,毕竟也确实不难。完整代码可以参考我的 PlainCEETimer 项目的相关文件,链接在文章末尾。

自定义对话框模板

首先我们得获得官方的模板,上面说了,在 SDK 里的 Font.Dlg 文件里,找不到的话用 Everything 通缉一下。

打开 Font.Dlg 后,我们会发现貌似有两个模板。这就是新版和旧版的 FontDialog,两个版本有什么区别呢?上面讲 CHOOSEFONT 时也说了,或者直接看代码和注释也能发现区别。

那我们就复制新版的好了,主要因为字体是新的,看着舒服。注意要完整复制其中的以下内容:

1
2
3
4
5
NEWFORMATDLGWITHLINK DIALOG 13, 54, 243, 234

// ...

END

接着打开项目里的 resource.h,添加以下引用和宏定义

1
2
3
4
5
6
#include <dlgs.h> // 包含控件 ID,确保自定义模板只更改样式/布局的同时保留功能

#define IDD_CHOOSEFONT 999 // 数值随意

#define LIBRARYNAME L"Natives.dll"
// 这个是当前项目的 DLL 的名字,如果是 EXE 的话就不用定义了

打开我们创建的 Natives 项目,单击 Natives.rc 资源文件 (没有的话新建一个),然后按下 F7 查看代码。接着找一个合适的位置 (最好在末尾),粘贴我们复制的模板,注意把开头的 NEWFORMATDLGWITHLINK 改成我们定义的 IDD_CHOOSEFONT。

接着我们就可以对它进行暴改了,比如删除一些自绘的样式或控件。

哪些样式与自绘有关?
FontDialog 的自绘重灾区主要就是所有的 ComboBox,相关的样式有 CBS_OWNERDRAWFIXED、CBS_HASSTRINGS。前者表示启用自绘,后者表示在自绘的情况下允许开发者可以通过 CB_GETLBTEXT 消息来获取到每个项的文本。我们不使用自绘,故两个都移除就行。

除了关闭自绘以外,我建议把前三个 ComboBox 的样式更改为 CBS_DROPDOWN,这样看着就是常见的样式,同时也保留了文本输入和匹配功能。而官方的 CBS_SIMPLE 样式就是真正意义上的 “Combo” -Box,由一个文本框和一个 ListBox 组成,而不是我们更熟悉的下拉样式。这样看似也能接受,实际上对在后期我们要应用深色主题时非常不友好,因为会有一个较粗的白边框。尽管可能有方法能修理,但从其他多个方面来说,还是更改为下拉框样式比较好。

注意控件的文本也需要更改,因为官方模板默认是英文的,而我们使用自定义模板后会失去 i18n (多语言) 支持。可以参考现有的 FontDialog 对照更改为中文。

然后我们保存并关闭打开的 resource.h 和 Natives.rc 标签页,双击项目的 Natives.rc 资源文件,会打开资源浏览器,不出意外的话就可以在 Dialog 目录下看到我们定义的对话框模板了。

然后双击对话框模板,可以打开设计器,移除我们不想要的控件,比如什么设置下划线,删除线、颜色什么的,以及下半部分那个很大的空白控件,可能有特殊用途,但是我发现删除了也没错误。

接着就是排版了,我是这么排的:

要注意的是,只要在设计器中打开过对话框模板,设计器会对其中一些参数进行修改,虽然调整了也不影响运行,但是保险起见还是与官方的保持一致。比如:设计器会把 DIALOG 改成 DIALOGEX,并在 FONT 后面加上 0, 0, 0x0 这样的参数,需要还原为:

1
2
3
4
IDD_* DIALOG // ...
// ...
FONT 9, "Segoe UI"
// ...

封装为 WinAPI 导出

前面我们说了,弹出 FontDialog 的核心代码就是调用了 ChooseFont 函数,为了尽可能减少在 C# 侧定义的大量 struct,我建议自己封装一个导出函数,在 C++ 侧完成对 CHOOSEFONT 的创建,也就是封装一下。

这个函数我们可以命名为 RunFontDialog,参数的话主要有:

  • HWND hWndOwner:指定父窗口的句柄
  • LPFRHOOKPROC lpfnHookProc:传入 HookProc 过程
  • LPLOGFONT lpLogFont:传入或取回选择的字体
  • int nSizeLimit:字体大小的范围

返回类型建议为 BOOL,可以用于判断对话框是否成功运行以及用户是否点击了确定来关闭对话框。

1
2
3
4
5
6
7
// CommonDialogs.h

#pragma once

#include <commdlg.h>

cexport(BOOL) RunFontDialog(HWND hWndOwner, LPFRHOOKPROC lpfnHookProc, LPLOGFONT lpLogFont, int nSizeLimit);

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// CommonDialogs.cpp

#include "pch.h"
#include "resource.h"
#include "CommonDialogs.h"

BOOL RunFontDialog(HWND hWndOwner, LPFRHOOKPROC lpfnHookProc, LPLOGFONT lpLogFont, int nSizeLimit)
{
if (hWndOwner && lpfnHookProc && lpLogFont && nSizeLimit > 0) // 确定各参数可用
{
CHOOSEFONT cf = { sizeof(cf) }; // sizeof 用于获取该结构体的大小
cf.hwndOwner = hWndOwner;
cf.lpLogFont = lpLogFont;
cf.Flags = CF_NOVERTFONTS | CF_TTONLY | CF_FORCEFONTEXIST | CF_LIMITSIZE | CF_SCRIPTSONLY | CF_INITTOLOGFONTSTRUCT | CF_ENABLEHOOK | CF_ENABLETEMPLATE; // 常用 Flag,上面讲过
cf.lpfnHook = lpfnHookProc;
cf.lpTemplateName = MAKEINTRESOURCE(IDD_CHOOSEFONT);
cf.hInstance = GetModuleHandleW(LIBRARYNAME);
cf.nSizeMin = LOWORD(nSizeLimit);
cf.nSizeMax = HIWORD(nSizeLimit);

BOOL result = ChooseFont(&cf);

if (result)
{
lpLogFont = cf.lpLogFont; // 用户确定后传回选择的字体
}

return result;
}

return FALSE;
}

初始化 CHOOSEFONT 时的 { sizeof(cf) } 写法是怎么回事?
该种写法表示将 sizeof(cf) 的值赋值给该 struct 的第一个第一个字段,刚好就对应了 lStructSize。也就是说在 { } 里,允许按顺序挨个给 struct 里的字段赋值而不显式指定字段名称,进而初始化结构体。在这里等效于 cf.lStructSize = sizeof(cf);

关于 lpTemplateNamehInstance
前面说过了,这两个字段用于指定对话框模板的名称以及所在的模块的句柄。填写方法也是有固定的套路,lpTemplateName 就用 MAKEINTRESOURCE 宏传入我们定义的 IDD_CHOOSEFONThInstance 就调用 GetModuleHandleW,若资源嵌入在我们的主程序里,就传入 nullptr,在这里我们是编译为 dll 用于 C# 调用,因此就要指定 dll 名称,就传入我们之前定义的 LIBRARYNAME 就行。

设置字体大小限制明明需要两个整数,为什么这里只用一个整数就传递了范围?
这算是一种编程技巧了,当我们需要传递多个 (别太多就行) 整型 (int 等) 以及布尔值 (可压缩为只占1位的 byte 类型) 时,就可以将它们合并为一个整型了,最后运算后的类型可以是 int、long 等,可以有效减少参数数量 (但是要注意每个数据在有效范围内,避免被截断)。这里的 nSizeLimit 的低位存储的是最小字体大小,提取时用 LOWORD 宏,高位是最大字体大小,用 HIWORD 宏提取。也不用担心会不够用,int 的高低位分别能存下最大 65535 的数值,足以表示字体大小。

这样我们就完成了对 ChooseFont 函数的封装,导出为了 RunFontDialog 函数,快去编译看看吧。

C# 二次封装

在 C++ 封装了还没完,如果我们需要在 WinForms 里调用这个 FontDialog 的话,也需要进行封装。由于 .NET 的 FontDialog 使用的是官方旧版的模板,我们需要自己做一个 FontDialog,比如命名为 MyFontDialog (之前提到过)。

首先声明导入函数:

1
2
3
[DllImport("Natives.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool RunFontDialog(IntPtr hWndOwner, WNDPROC lpfnHookProc, ref LOGFONT lpLogFont, int nSizeLimit);

或者这样:

1
2
[DllImport("Natives.dll")]
public static extern BOOL RunFontDialog(HWND hWndOwner, WNDPROC lpfnHookProc, ref LOGFONT lpLogFont, int nSizeLimit);

这里用到了一些 Win32 类型 (BOOL、HWND,而 WNDPROC 在之前已经定义过了),可以看看 在 C# 里引入 C++ 类型,增强 Interop 体验 这篇文章。

为什么 C++ 的 LPFRHOOKPROC 类型在 C# 里就变成了 WNDPROC?
两者都表示函数指针,也就是委托,且签名都相同,只是名字不同而已,也是可以正常运行的。

还用到了 LOGFONT,这是 Win32 的字体类型,我们也可以实现与 .NET 的 Font 类型互相转换,.NET 也为我们提供了相应的转换方法 (只不过 FromLogFont 返回的的字体大小的单位是 px,我们需要以 pt 的方式重新构造一下),也不复杂。定义即转换如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct LOGFONT
{
public int lfHeight;
public int lfWidth;
public int lfEscapement;
public int lfOrientation;
public int lfWeight;
public byte lfItalic;
public byte lfUnderline;
public byte lfStrikeOut;
public byte lfCharSet;
public byte lfOutPrecision;
public byte lfClipPrecision;
public byte lfQuality;
public byte lfPitchAndFamily;

[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string lfFaceName;

public readonly Font ToFont() // 将 LOGFONT 转换为 Font
{
using var font = Font.FromLogFont(this);
return new(font.FontFamily, font.SizeInPoints, font.Style, GraphicsUnit.Point, font.GdiCharSet, font.GdiVerticalFont);
}

public static LOGFONT FromFont(Font font) // 将 Font 转换为 LOGFONT
{
LOGFONT lf = new();
object lfobj = lf;
font.ToLogFont(lfobj);
return (LOGFONT)lfobj;
}
}

当然我们在构造 nSizeLimit 时也需要一些方法,在 Win32 里我们可以用 MAKELONG 宏来完成,在 C# 里的话虽然可以直接写运算式,但为了语义性还是封装为方法比较好,这里就顺便用一下 C# 14 的扩展成员语法糖:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static class Extensions
{
extension(int value)
{
public ushort LoWord => (ushort)(value & 0xFFFF);
public ushort HiWord => (ushort)((value >> 16) & 0xFFFF); // & 0xFFFF 可以省略

public static int MakeLong(ushort low, ushort high)
{
return low | (high << 16);
}
}
}

为什么无论组合还是提取,都与 16 有关?
因为 int 本来是 32 位 (Int32),也就是有 32 个位,砍半分别来存放高低位正好就是 16 (UInt16)。0xFFFF 只是语义上的直观表示了位,具体数值 (65535, UInt16 最大值) 没有太大意义,一个 F 表示 4 个位,FFFF 就是 16 个位。

int 的高低位怎么组合?
这就涉及到位运算了,详细教程可以参考网络,这里我就简单讲一下。首先大家得知道高低位在哪里?拿出草稿纸,我们来手写一个二进制数,比如 1010,是从左往右书写的,所以左边是低位,右边是高位?戳!还记得我们在小学二年级 (高中) 学的怎么把二进制数转换为十进制数吗?是不是从右往左的运算方式?也就是用每个位上的数字乘以2的相应次幂: 0×2⁰+1×2¹+0×2²+1×2³=10,因此 1010 表示 10,所以低位在右边,高位在左边。这个例子只是让大家回忆一下并理解高低位在哪里,实际我们这里不需要进制转换。然后我们需要知道 | 运算符的作用,学名是按位或 (Bitwise OR),我们直接来看例子:0b01 | 0b10 就等于 0b11,也就是把0全部填充为了 1;那 0b11 | 0b01 呢?也是 0b11,因为两者的低位都是 1,而高位分别是 1 和 0,结果为 1。难理解的话把它看成布尔运算符 ||,这个我们更常用。我们规定 0=false,1=true,那你说 false || false 是不是 false?false || true 是不是 true?true || true 是不是还是 true?这很类似于取并集。接着就是 << 了,学名叫左移运算符,相反也有 >>,后面就不讲了。比如 0b1 也就是 0b0001 被 << 2 后就变成 0b0100,更好理解对吧。为什么要左移?前面说了,左边是高位,计算机默认从低位开始填充,因此我们的 high << 16 就是把 high 的数值推到低 16 位之后,也就是腾出 16 个位 (有点晕的话草稿纸上画个示意图),使它自己成为高位;最后再 | 上 low 就组合到一起了,因为 low 默认就是低位且我们没有处理,就自然而然地被填充到了低位,也就完成了组合。

int 的高低位怎么提取?
先说说高位,因为最简单,直接 >> 16 就行。想象一下这个过程,int 本来就是 32 位的,把高 16 位直接右移 16 位使之成为低 16 位,同时之前的高 16 位全部变成了 0,是不是刚好把原来的低 16 位挤掉了?所以又自然而然地提取到了高 16 位,转换为 ushort 是强制再次截断低 16 位以外的位 (这也就是为什么此处的 & 0xFFFF 可以省略)。低位的提取其实也简单,& 学名按位与 (Bitwise AND),0b01 & 0b10 等于 0,0b11 & 0b01 等于 0b01,不能理解的话还是类比布尔运算符 &&,false && false=false,false && true=false,true && true=true,有点类似于取交集:0b01 和 0b10 有重叠的吗?没有,那结果就为 0。那这里的 & 0xFFFF 是什么呢?小学二年级的都知道,这是十六进制数,对应二进制正好是 0b1111_1111_1111_1111,也就是 65535。将一个数 & 0xFFFF 后,表示取其低 16 位,其他位全部被截断,进而就获取到了低位。

其实这个扩展成员还有一种写法,因为我们常用 int,ushort 比较少用,于是我们可以将 ushort 全部替换为 int,并做一些调整

1
2
3
4
5
6
7
8
9
10
extension(int value)
{
public int LoWord => value & 0xFFFF;
public int HiWord => (value >> 16) & 0xFFFF; // 这个 & 0xFFFF 最好不要省略

public static int MakeLong(int low, int high)
{
return (ushort)low | ((ushort)high << 16); // 位运算时仍推荐使用 ushort 截断,相当于 & 0xFFFF
}
}

现在就要改写我们的 MyFontDialog 了,在此之前先想想应该继承自谁?

继续继承 .NET 的 FontDialog?不行。FontDialog 本来就有默认实现,派生类虽然可以重写忽略,但还有其他的机制无法形成连贯,所以不行。

不继承行不行?(隐式继承 Object) 貌似可行,但在 WinForms 这样的托管环境中弹出非托管对话框需要特殊处理,不建议直接弹出,也不行。

那只有一种可能了,直接继承自 CommonDialog 类,就像 WinForms 自己的 FontDialog 也继承它一样。而前面也说过了,我们实现的 RunDialog 会在基类的 ShowDialog(IWin32Window) 中被调用,其中就包含了处理非托管对话框的机制 (最最主要的是启动一个模态消息循环)。

CommonDialog 是 WinForms 提供的抽象基类,要求派生类必须实现 RunDialog 和 Reset 这两个抽象方法,可选重写 HookProc 等虚成员。

由于我们没有继承自 FontDialog,所以没有了 FontDialog 的一系列的常用属性,所有常用的属性我们都在 C++ 里实现了,C# 这里只对外公开一个 Font 属性就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public sealed class MyFontDialog(Font existing) : CommonDialog
{
public Font Font => _font;

private CommonDialogHelper Helper;
private Font _font = existing; // existing 表示传入的默认选择的字体

public DialogResult ShowDialog(MyFormBase owner) // MyFormBase 是之前提到过的,是我们应用程序里所有窗体的基类
{
Helper = new(this, owner, "选择字体 - 高考倒计时"); // 由于我们完全自行封装了 FontDialog,所以也不需要给 Helper 传入 DefHookProc 了
return base.ShowDialog(owner);
}

protected override IntPtr HookProc(IntPtr hWnd, int msg, IntPtr wparam, IntPtr lparam)
{
return Helper.HookProc(hWnd, msg, wparam, lparam);
}

protected override bool RunDialog(IntPtr hwndOwner)
{

}

public override void Reset()
{
_font = default;
}
}

这里 Reset 方法我们实际上可以留空,因为这个方法原本是用于重置设置的,然而我们的 MyFontDialog 并未开放任何设置项,都在 C++ 里写死了,因此就没必要了,如果各位有需求的话也可以自行尝试一下。这里我们还是假装重置一下 _font 为 null。

其余的代码也在之前说过了,这里就不讲了。我们来讲一下 RunDialog 怎么写。

总之我们的目的就是要运行我们导出的那个 RunFontDialog 函数,其实写出来就明白了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected override bool RunDialog(IntPtr hwndOwner)
{
try
{
var lf = LOGFONT.FromFont(_font);
var result = CommonDialogs.RunFontDialog(hWndOwner, HookProc, ref lf, int.MakeLong(10, 36));

if (result)
{
_font = lf.ToFont();
}

return result;
}
catch
{
return false;
}
}

为什么要用 try…catch… ?因为这里有时会遇到 LOGFONT 转 Font 报错的情况,提示非 TTF 字体,但是我们明明就在 C++ 那里指定了只包含 TTF 字体,还是有点奇怪,估计是 WinForms 底层有 Bug 或者是其他未知问题。

至此大功告成,运行看看效果:(以我在 PlainCEETimer 里实现的为例)

如果以后还有机会的话,我会尝试是否可以更改那个预览部分的前背景色。

大家也可以尝试按照同样的套路自行实现一下 ColorDialog (也可以参考我的实现,在文章末尾的链接里),这里由于时间问题,就不多讲解了。

参考


自定义 Font/ColorDialog 等常用对话框样式
https://wanghaonie.github.io/posts/3f71e409b6a3/
作者
WangHaonie
发布于
2025-08-28 19:34:25
更新于
2025-09-05 07:45:38
许可协议