.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能正常編譯):猛擊我


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM