.NET Windows Form 改变窗体类名(Class Name)有多难?
研究WinForm的东西,是我的一个个人兴趣和爱好,以前做的项目,多与WinForm相关,然而这几年,项目都与WinForm没什么关系了,都转为ASP.NET MVC与WPF了。关于今天讨论的这个问题,以前也曾深入研究过,只是最近有朋友问到这个问题,就再挖挖这个坟(坑)。
一、类名是啥?
打开神器SPY++,VS2013 在【工具】菜单里:
VS2013之前的VS版本,在【开始菜单】里:
打开SPY++,点击标注的按钮,
在打开的窗口上,把雷达按钮拖到你想查看的窗口,就可以看到它的类名了,下面就是QQ的类名:
再看看.NET WinForm的窗体类名:
一大串啊,有没有,我不想这样,我想要一个有个性的、简单的类名,咋办?
二、 不是有个CreateParams属性吗?
作为一个有多年WinForm开发经验的程序猿,这有啥难的,WinForm的控件不是都有个CreateParams属性吗?里面可以不是就可以设置窗口类名吗?看看:
真的有,这不就简单了嘛,动手,于是有下面代码:
public partial class FormMain : Form { public FormMain() { InitializeComponent(); } protected override CreateParams CreateParams { get { CreateParams createParams = base.CreateParams; createParams.ClassName = "Starts2000.Window"; //这就是我想要的窗体类名。 return createParams; } } }
编译,运行,结果却是这样的:
泥煤啊,这是什么啊,翻~墙,一通谷歌,原来类名使用前都需要注册啊,难道微软只注册了自己的类名,我个性化的他就不帮我注册,那我就自己注册吧,坑爹的微软啊。
三、注册一个窗口类名吧
注册窗口类名需要用到Windows API函数了,用C#进行P/Invoke?太麻烦了,做了这么多年的WinForm开发,我可是练了《葵花宝典(C++/CLI)》的,只是因为没自宫,所以没大成,不过,简单用用还是可以的。
创建一个C++空项目,设置项目属性-配置属性-常规,如下图:
于是有了下面的代码:
1. FormEx.h
#pragma once #include <Windows.h> #include <vcclr.h> #define CUSTOM_CLASS_NAME L"Starts2000.Window" namespace Starts2000 { namespace WindowsClassName { namespace Core { using namespace System; using namespace System::Windows::Forms; using namespace System::Runtime::InteropServices; private delegate LRESULT WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); public ref class FormEx : public Form { public: static FormEx(); FormEx(); private: static LRESULT WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); static void ProcessExit(Object^ sender, EventArgs^ e); }; } } }
2. FormEx.cpp
#include "FormEx.h" namespace Starts2000 { namespace WindowsClassName { namespace Core { static FormEx::FormEx() { WNDCLASSEX wc; Starts2000::WindowsClassName::Core::WndProc ^windowProc = gcnew Starts2000::WindowsClassName::Core::WndProc(FormEx::WndProc); pin_ptr<Starts2000::WindowsClassName::Core::WndProc^> pWindowProc = &windowProc; ZeroMemory(&wc, sizeof(WNDCLASSEX)); wc.cbSize = sizeof(WNDCLASSEX); wc.style = CS_DBLCLKS; wc.lpfnWndProc = reinterpret_cast<WNDPROC>(Marshal::GetFunctionPointerForDelegate(windowProc).ToPointer()); wc.hInstance = GetModuleHandle(NULL); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)COLOR_WINDOW; //(HBRUSH)GetStockObject(HOLLOW_BRUSH); wc.lpszClassName = CUSTOM_CLASS_NAME; ATOM classAtom = RegisterClassEx(&wc); DWORD lastError = GetLastError(); if (classAtom == 0 && lastError != ERROR_CLASS_ALREADY_EXISTS) { throw gcnew ApplicationException("Register window class failed!"); } System::AppDomain::CurrentDomain->ProcessExit += gcnew System::EventHandler(FormEx::ProcessExit); } FormEx::FormEx() : Form() { } LRESULT FormEx::WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { System::Windows::Forms::Message message = System::Windows::Forms::Message::Create((IntPtr)hWnd, (int)msg, (IntPtr)((void*)wParam), (IntPtr)((void*)lParam)); System::Diagnostics::Debug::WriteLine(message.ToString()); return DefWindowProc(hWnd, msg, wParam, lParam); } void FormEx::ProcessExit(Object^ sender, EventArgs^ e) { UnregisterClass(CUSTOM_CLASS_NAME, GetModuleHandle(NULL)); } } } }
3. 创建一个C# WinForm项目,引用上面创建的C++/CLI项目生成的DLL,代码跟最开始的区别不大。
public partial class FormMain : /*Form*/ FormEx { public FormMain() { InitializeComponent(); } protected override CreateParams CreateParams { get { CreateParams createParams = base.CreateParams; createParams.ClassName = "Starts2000.Window"; //这就是我想要的窗体类名。 return createParams; } } }
编译,运行,结果却仍然是这样的:
泥煤啊,微软到底干了什么,我只是想搞点小玩意,满足下我的虚荣心,你竟然……,心中千万头“羊驼”奔腾而过。
没办法了,微软不都开源了吗,也不需要反编译了,直接下源代码看吧。
四、也不反编译了,直接找源代码看吧
在微软的网站(http://referencesource.microsoft.com/)Down下代码,从Form→ContainerControl→ScrollableControl→Control,在Control里找到NativeWindow,再在NativeWindow里面找到了WindowClass,在WindowClass里找到了坑爹的RegisterClass方法,恍然大悟了,有没有,具体看代码,我加了注释。
private void RegisterClass() { NativeMethods.WNDCLASS_D wndclass = new NativeMethods.WNDCLASS_D(); if (userDefWindowProc == IntPtr.Zero) { string defproc = (Marshal.SystemDefaultCharSize == 1 ? "DefWindowProcA" : "DefWindowProcW"); userDefWindowProc = UnsafeNativeMethods.GetProcAddress(new HandleRef(null, UnsafeNativeMethods.GetModuleHandle("user32.dll")), defproc); if (userDefWindowProc == IntPtr.Zero) { throw new Win32Exception(); } } string localClassName = className; if (localClassName == null) { //看看是否自定义了classnName,就是我们在 CreateParams ClassName设置的值。 // If we don't use a hollow brush here, Windows will "pre paint" us with COLOR_WINDOW which // creates a little bit if flicker. This happens even though we are overriding wm_erasebackgnd. // Make this hollow to avoid all flicker. // wndclass.hbrBackground = UnsafeNativeMethods.GetStockObject(NativeMethods.HOLLOW_BRUSH); //(IntPtr)(NativeMethods.COLOR_WINDOW + 1); wndclass.style = classStyle; defWindowProc = userDefWindowProc; localClassName = "Window." + Convert.ToString(classStyle, 16); hashCode = 0; } else { //坑爹的就在这里了 NativeMethods.WNDCLASS_I wcls = new NativeMethods.WNDCLASS_I(); /*注意下面这句代码,特别注意 NativeMethods.NullHandleRef,MSDN说明: * BOOL WINAPI GetClassInfo( * _In_opt_ HINSTANCE hInstance, * _In_ LPCTSTR lpClassName, * _Out_ LPWNDCLASS lpWndClass * ); * hInstance [in, optional] * Type: HINSTANCE * A handle to the instance of the application that created the class. * To retrieve information about classes defined by the system (such as buttons or list boxes), * set this parameter to NULL. * 就是说,GetClassInfo 的第一个参数为 NULL(NativeMethods.NullHandleRef)的时候,只有系统注册的 ClassName * 才会返回 True,所以当我们设置了CreateParams ClassName的值后,只要设置的不是系统注册的 ClassName,都会 * 抛出后面的 Win32Exception 异常,泥煤啊。 */ bool ok = UnsafeNativeMethods.GetClassInfo(NativeMethods.NullHandleRef, className, wcls); int error = Marshal.GetLastWin32Error(); if (!ok) { throw new Win32Exception(error, SR.GetString(SR.InvalidWndClsName)); } wndclass.style = wcls.style; wndclass.cbClsExtra = wcls.cbClsExtra; wndclass.cbWndExtra = wcls.cbWndExtra; wndclass.hIcon = wcls.hIcon; wndclass.hCursor = wcls.hCursor; wndclass.hbrBackground = wcls.hbrBackground; wndclass.lpszMenuName = Marshal.PtrToStringAuto(wcls.lpszMenuName); localClassName = className; defWindowProc = wcls.lpfnWndProc; hashCode = className.GetHashCode(); } // Our static data is different for different app domains, so we include the app domain in with // our window class name. This way our static table always matches what Win32 thinks. // windowClassName = GetFullClassName(localClassName); windowProc = new NativeMethods.WndProc(this.Callback); wndclass.lpfnWndProc = windowProc; wndclass.hInstance = UnsafeNativeMethods.GetModuleHandle(null); wndclass.lpszClassName = windowClassName; short atom = UnsafeNativeMethods.RegisterClass(wndclass); if (atom == 0) { int err = Marshal.GetLastWin32Error(); if (err == NativeMethods.ERROR_CLASS_ALREADY_EXISTS) { // Check to see if the window class window // proc points to DefWndProc. If it does, then // this is a class from a rudely-terminated app domain // and we can safely reuse it. If not, we've got // to throw. NativeMethods.WNDCLASS_I wcls = new NativeMethods.WNDCLASS_I(); bool ok = UnsafeNativeMethods.GetClassInfo(new HandleRef(null, UnsafeNativeMethods.GetModuleHandle(null)), windowClassName, wcls); if (ok && wcls.lpfnWndProc == NativeWindow.UserDefindowProc) { // We can just reuse this class because we have marked it // as being a nop in another domain. All we need to do is call SetClassLong. // Only one problem: SetClassLong takes an HWND, which we don't have. That leaves // us with some tricky business. First, try this the easy way and see // if we can simply unregister and re-register the class. This might // work because the other domain shutdown would have posted WM_CLOSE to all // the windows of the class. if (UnsafeNativeMethods.UnregisterClass(windowClassName, new HandleRef(null, UnsafeNativeMethods.GetModuleHandle(null)))) { atom = UnsafeNativeMethods.RegisterClass(wndclass); // If this fails, we will always raise the first err above. No sense exposing our twiddling. } else { // This is a little harder. We cannot reuse the class because it is // already in use. We must create a new class. We bump our domain qualifier // here to account for this, so we only do this expensive search once for the // domain. do { domainQualifier++; windowClassName = GetFullClassName(localClassName); wndclass.lpszClassName = windowClassName; atom = UnsafeNativeMethods.RegisterClass(wndclass); } while (atom == 0 && Marshal.GetLastWin32Error() == NativeMethods.ERROR_CLASS_ALREADY_EXISTS); } } } if (atom == 0) { windowProc = null; throw new Win32Exception(err); } } registered = true; }
五、吓尿了!自己动手,丰衣足食
看到微软的源码后,只能表示尿了,不可能继承Form实现自定义类名了,那么就自己动手,丰衣足食吧,还记得上面的C++/CLI代码吧,简单的加一些内容,就可以实现我们自定义窗口类名的愿望了,代码如下:
1. CustomForm.h
#pragma once #include <Windows.h> #include <vcclr.h> #define CUSTOM_CLASS_NAME L"Starts2000.Window" namespace Starts2000 { namespace WindowsClassName { namespace Core { using namespace System; using namespace System::Windows::Forms; using namespace System::Runtime::InteropServices; private delegate LRESULT WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); public ref class CustomForm { public: static CustomForm(); CustomForm(); CustomForm(String ^caption); ~CustomForm(); void Create(); void Show(); private: String ^_caption; HWND _hWnd; static GCHandle _windowProcHandle; static LRESULT WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); static void ProcessExit(Object^ sender, EventArgs^ e); }; } } }
2. CustomForm.cpp
#include "CustomForm.h" namespace Starts2000 { namespace WindowsClassName { namespace Core { static CustomForm::CustomForm() { WNDCLASSEX wc; Starts2000::WindowsClassName::Core::WndProc ^windowProc = gcnew Starts2000::WindowsClassName::Core::WndProc(CustomForm::WndProc); _windowProcHandle = GCHandle::Alloc(windowProc); ZeroMemory(&wc, sizeof(WNDCLASSEX)); wc.cbSize = sizeof(WNDCLASSEX); wc.style = CS_DBLCLKS; wc.lpfnWndProc = reinterpret_cast<WNDPROC>(Marshal::GetFunctionPointerForDelegate(windowProc).ToPointer()); wc.hInstance = GetModuleHandle(NULL); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)COLOR_WINDOW; //(HBRUSH)GetStockObject(HOLLOW_BRUSH); wc.lpszClassName = CUSTOM_CLASS_NAME; ATOM classAtom = RegisterClassEx(&wc); DWORD lastError = GetLastError(); if (classAtom == 0 && lastError != ERROR_CLASS_ALREADY_EXISTS) { throw gcnew ApplicationException("Register window class failed!"); } System::AppDomain::CurrentDomain->ProcessExit += gcnew System::EventHandler(CustomForm::ProcessExit); } CustomForm::CustomForm() : _caption("Starts2000 Custom ClassName Window") { } CustomForm::CustomForm(String ^caption) : _caption(caption) { } CustomForm::~CustomForm() { if (_hWnd) { DestroyWindow(_hWnd); } } void CustomForm::Create() { DWORD styleEx = 0x00050100; DWORD style = 0x17cf0000; pin_ptr<const wchar_t> caption = PtrToStringChars(_caption); _hWnd = CreateWindowEx(styleEx, CUSTOM_CLASS_NAME, caption, style, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, GetModuleHandle(NULL), NULL); if (_hWnd == NULL) { throw gcnew ApplicationException("Create window failed! Error code:" + GetLastError()); } } void CustomForm::Show() { if (_hWnd) { ShowWindow(_hWnd, SW_NORMAL); } } LRESULT CustomForm::WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { System::Windows::Forms::Message message = System::Windows::Forms::Message::Create((IntPtr)hWnd, (int)msg, (IntPtr)((void*)wParam), (IntPtr)((void*)lParam)); System::Diagnostics::Debug::WriteLine(message.ToString()); if (msg == WM_DESTROY) { PostQuitMessage(0); return 0; } return DefWindowProc(hWnd, msg, wParam, lParam); } void CustomForm::ProcessExit(Object^ sender, EventArgs^ e) { UnregisterClass(CUSTOM_CLASS_NAME, GetModuleHandle(NULL)); if (CustomForm::_windowProcHandle.IsAllocated) { CustomForm::_windowProcHandle.Free(); } } } } }
最后仍然用我们熟悉的C#来调用:
using System; using System.Windows.Forms; using Starts2000.WindowsClassName.Core; namespace Starts2000.WindowClassName.Demo { static class Program { /// <summary> /// 应用程序的主入口点。 /// </summary> [STAThread] static void Main() { //Application.EnableVisualStyles(); //Application.SetCompatibleTextRenderingDefault(false); //Application.Run(new FormMain()); CustomForm form = new CustomForm(); form.Create(); form.Show(); Application.Run(); } } }
编译,运行,拿出神器SPY++看一看:
目标终于达成。
最后,所有代码的下载(项目使用的是VS2013编译、调试,不保证其他版本VS能正常编译):猛击我。