【C#/WinForms】タイトルバーを消して独自に実装する3つの方法

C#
この記事は約14分で読めます。

Windows Formアプリケーションのタイトルバーを消したり独自に実装する方法のまとめです。

独自に実装する前に…

最近だと公開APIを利用するとタイトルバーを黒にできます。独自に実装とかかなり面倒なので一度ご確認頂ければと思います。

公開APIを試してみた結果です。黒にするだけであればこれで十分かもしれませんね。

ここからは独自にタイトルバーを作成したい人向けの内容になります。

ウィンドウの基本構成

まずウィンドウの基本構成です。タイトルバーの高さはウィンドウの枠を含めて31pxで構成されています。タイトルバーの構成要素は下記です。

タイトルバーの高さ = タイトルバーの文字列の高さ(23px) + ウィンドウ枠幅(4px) + ウィンドウ枠の余白(4px)

※Windows10の場合

タイトルバーの文字列の高さ取得方法

タイトルバーの文字列の高さは SystemInformation.CaptionHeight で取得します。私の環境(Windows10)の場合、SystemInformation.CaptionHeight は 23px でした。

ウィンドウ枠幅とウィンドウ枠の余白の取得方法

ウィンドウの枠は SystemInformation.FrameBorderSize で取得します。ただし、ウィンドウ枠の余白は SystemInformation からは取得できないため、Win32API を利用して取得します。私の環境の場合、SystemInformation.FrameBorderSize は 4px、ウィンドウ枠の余白は 4pxでした。また、DPI を考慮した取得方法は下記になります。

[DllImport("user32.dll")]
public static extern int GetSystemMetricsForDpi(int nIndex, int dpi);

// タイトルバーの高さ
public static int GetTitleBarHeight(int dpi) {
    const int SM_CYCAPTION = 0x04;
    return GetSystemMetricsForDpi(SM_CYCAPTION , dpi);
}

// ウィンドウ枠
public static Size GetFrameBorderSize(int dpi) { 
    const int SM_CXFRAME = 0x20;
    const int SM_CYFRAME= 0x21;
    int width = GetSystemMetricsForDpi(SM_CXFRAME, dpi);
    int height = GetSystemMetricsForDpi(SM_CYFRAME, dpi);

    return new Size(width, height);
}

// ウィンドウ枠の余白
public static int GetPaddingBorder(int dpi) {
    const int SM_CXPADDEDBORDER = 92;
    return GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi);
}

タイトルバーのカスタマイズ方法

方法1(FormBorderStyle に FormBorderStyle.None を設定する)

【C#】タイトルバーを独自に実装してダークモードを適用する方法で紹介している方法です。標準機能を利用するならこの方法でタイトルバーを消して、タイトルバーを ToolStrip で作るのが一番簡単です。ただし、ウィンドウの移動やサイズ変更はできないため、マウスカーソルの位置に応じて独自に実装する必要があります。

また、この方法だとウィンドウの影が消える(Windows10の場合)ことやサイズ変更時のアニメーションはしなくなります。

方法2(タイトルバー(非クライアント領域)を独自に描画する)

次はタイトルバー自体を描画する方法です。非クライアント領域とはウィンドウの枠やタイトルバーの領域(コントロールの配置ができない領域)のことです。

タイトルバーの描画は WM_NCPAINT メッセージで行われるため、このメッセージ内で独自にタイトルバーを描画します。この方法の場合、ウィンドウの移動やサイズ変更も独自に実装する必要はありません。ウィンドウのサイズ変更時のアニメーションもします。

using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace Sample{
    public partial class Form1 : Form {

        private const int WM_ACTIVATE = 0x0006;
        private const int WM_NCPAINT = 0x0085;
        private const int WM_NCACTIVATE = 0x0086;

        [StructLayout(LayoutKind.Sequential)]
        struct RECT {
            public int Left;
            public int Top;
            public int Right;
            public int Bottom;
            public Size Size { get { return new Size(Right - Left, Bottom - Top); } }
        }

        [DllImport("user32.dll")]
        static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);

        [DllImport("user32.dll")]   
        private static extern IntPtr GetWindowDC(IntPtr hWnd);

        [DllImport("user32.dll")]   
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);

        [DllImport("gdi32.dll")]
        static extern IntPtr CreateRectRgn(int left, int top, int right, int bottom);

        [DllImport("gdi32.dll")]
        static extern bool DeleteObject(IntPtr hObject);

        public Form1() {
            InitializeComponent();
        }

        /// <summary>
        /// WndProc
        /// </summary>
        /// <param name="m"></param>
        protected override void WndProc(ref Message m) {

            switch (m.Msg) {
                case WM_ACTIVATE:
                case WM_NCPAINT:
                case WM_NCACTIVATE:
                    // 非クライアント領域を含むデバイスコンテキストを取得
                    IntPtr hdc = GetWindowDC(m.HWnd);
                    try {
                        // デバイスコンテキストからGraphicsを生成
                        // Regionはクリッピングに使用
                        using (Graphics g = Graphics.FromHdc(hdc))
                        using (Region rgn = new Region()) {
                            RECT rect;

                            // ウィンドウサイズを取得
                            // Sizeプロパティでは正確な値が取れないので
                            GetWindowRect(m.HWnd, out rect);

                            // コントロールの矩形
                            Rectangle clientRect = new Rectangle(Point.Empty, rect.Size);

                            // 境界線の太さ分収縮した矩形を描画対象から外す
                            rgn.Union(clientRect);
                            rgn.Xor(new Rectangle(8, 31, clientRect.Width - 16, clientRect.Height - 39));
                            g.Clip = rgn;

                            // タイトルバー領域の塗りつぶし
                            g.FillRectangle(Brushes.Green, clientRect);

                            // WParamにはクリッピング領域のリージョンハンドルを設定
                            // OSによる描画範囲を境界線の太さ分だけ収縮した矩形とする
                            IntPtr wParam = m.WParam;
                            m.WParam = CreateRectRgn(rect.Left + 8, rect.Top + 31, rect.Right - 8, rect.Bottom - 39);

                            base.WndProc(ref m);

                            // リージョンを削除し、WParamの値を元に戻す
                            DeleteObject(m.WParam);
                            m.WParam = wParam;
                        }
                    } catch {
                        // 例外が発生したらOSに描画させる
                        base.WndProc(ref m);
                    } finally {
                        // 取得したデバイスコンテキストを解放
                        ReleaseDC(m.HWnd, hdc);
                    }
                    return;

                default:
                    base.WndProc(ref m);
                    return;
            }
        }
    }
}

ただし、ウィンドウの影は消えてしまいます。下記フォームは WM_NCPAINT メッセージでウィンドウの枠とタイトルバーを緑で塗り潰しています。

閉じるボタンなども塗り潰されますが、マウスカーソルを元々あった閉じるボタンの上に持っていくと閉じるボタンなどが描画されます。

つまり、閉じるボタンなども考慮して実装する必要があります。

方法3(クライアント領域を広げる)

最後はクライアント領域を広げる方法です。クライアント領域とはタイトルバーやウィンドウの枠を除いた領域(コントロールの配置が可能な領域)のことです。

クライアント領域を非クライアント領域(タイトルバーの部分)まで広げ、タイトルバーを ToolStrip で実装します。下記サンプルはクライアント領域を上に 31px 広げてタイトルバーを消しています。

public const int WM_NCCALCSIZE = 0x83;
public const int WVR_VALIDRECTS = 0x0400;

[StructLayout(LayoutKind.Sequential)]
public struct NCCALCSIZE_PARAMS {
    public RECT rcNewWindow;
    public RECT rcOldWindow;
    public RECT rcClient;
    IntPtr lppos;
}

[StructLayout(LayoutKind.Sequential)]
public struct RECT {
    public int Left;
    public int Top;
    public int Right;
    public int Bottom;
    public int Width() {
        return Right - Left;
    }
    public int Height() {
        return Bottom - Top;
    }
    public Size Size { 
        get { 
            return new Size(Right - Left, Bottom - Top); 
        }
    }
}

protected override void WndProc(ref Message m) {
    switch (m.Msg) {
        // クライアント領域計算
        case WM_NCCALCSIZE:
            if (m.WParam != IntPtr.Zero && m.Result == IntPtr.Zero) {
                NCCALCSIZE_PARAMS nc = (NCCALCSIZE_PARAMS)Marshal.PtrToStructure(m.LParam, typeof(NCCALCSIZE_PARAMS));
                nc.rcNewWindow.Top -= SystemInformation.CaptionHeight + SystemInformation.FrameBorderSize.Height + GetPaddingBorder();
                nc.rcOldWindow = nc.rcNewWindow;

                Marshal.StructureToPtr(nc, m.LParam, false);
                m.Result = (IntPtr)WVR_VALIDRECTS;
                base.WndProc(ref m);

            } else {
                base.WndProc(ref m);
            }
            break;

        default: {
            base.WndProc(ref m);
            break;
        }
    }
}

この方法の場合、ウィンドウの移動やサイズ変更は独自に実装する必要がありますが、標準のウィンドウの見た目を維持したまま、タイトルバーを消すことができます。つまりウィンドウの影も消えません。試しにクライアント領域を広げて ToolStrip でタイトルバーを実装したものが下記になります。

サンプルアプリとソースコードは BOOTH で販売しています。

方法3の問題点

方法3の場合はフォームを最大化して元に戻すとフォームの高さが元より大きくなるようです。そのため、下記のように元に戻す際に最大化前のサイズに戻してください。

private void button_Click(object sender, EventArgs e) {
    if (this.WindowState == FormWindowState.Normal) {
        this.WindowState = FormWindowState.Maximized;
    } else {
        this.Size = this.RestoreBounds.Size;
        this.WindowState = FormWindowState.Normal;
    }
}

おわりに

余り見た目を気にせず簡単に実装するなら、方法1。とにかく独自に細部にこだわって実装するなら方法2。各OS毎の標準フォームの見た目を維持したままタイトルバーを実装するなら方法3ですね。

ダークモードフォームのバグやご要望などはフォーラムからお願いいたします。

コメント

タイトルとURLをコピーしました