今からでも間に合う

技術を学ぶのは今からでも遅くない

C#でタイトルバーをカスタマイズする

WinFormsでタイトルバーをカスタマイズしてみます。

カスタマイズするためには

いろいろなやり方があるようです。

  1. FormBorderStyle.Noneにする
  2. 独自に描画する
  3. クライアント領域を拡張する

やり方については以下のサイトを参考に「クライアント領域を拡張する」で試してみました。

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

クライアント領域を拡張する

最初はこんな感じからスタート。
境界をわかりやすくするため、BackColorを変えています。

初期状態

クライアント領域を拡張してみます。
まずはWinApiを扱う部分。

private static class NativeMethods
{
    public const int SM_CXPADDEDBORDER = 92;

    [DllImport("user32.dll")]
    public static extern int GetSystemMetrics(int nIndex);

    public static int GetPaddingBorder() => GetSystemMetrics(SM_CXPADDEDBORDER);
    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() => Right - Left;
        public int Height() => Bottom - Top;
        public Size Size => new Size(Right - Left, Bottom - Top);
    }
}

続いて、WndProc。デザイナの邪魔しないようにDesignModeの判定も追加しています。

protected override void WndProc(ref Message m)
{
    switch (m.Msg)
    {
        case NativeMethods.WM_NCCALCSIZE:
            if (!DesignMode && m.WParam != IntPtr.Zero && m.Result == IntPtr.Zero)
            {
                NativeMethods.NCCALCSIZE_PARAMS nc = (NativeMethods.NCCALCSIZE_PARAMS)Marshal.PtrToStructure(m.LParam, typeof(NativeMethods.NCCALCSIZE_PARAMS));
                nc.rcNewWindow.Top -= SystemInformation.CaptionHeight + SystemInformation.FrameBorderSize.Height + NativeMethods.GetPaddingBorder();
                nc.rcOldWindow = nc.rcNewWindow;

                Marshal.StructureToPtr(nc, m.LParam, false);
                m.Result = (IntPtr)NativeMethods.WVR_VALIDRECTS;
                base.WndProc(ref m);
                return;
            }
            break;
        default:
            break;
    }
    base.WndProc(ref m);
}

この状態で実行するとこうなります。
結局わかりにくいですが、最初の画像と高さは同じです。

クライアント領域を拡張した状態

拡張した領域はどのように扱われるのか

システム操作が行えなくなる

タイトルバー領域を上書きしているので、タイトルバーに対して行えていた操作(移動、最大化、閉じるといった操作)ができなくなります。

通常のクライアント領域と同等に扱える

拡張しているのでそのまんまですね。
例えばデザイナでこのようにすると、

ボタンを配置
こうなります。
タイトルバーがあった位置にボタン

タイトルバーっぽくしていく

ToolStripでダークモードっぽく

ダークモードっぽくToolStripを配置して、タイトルバーに見えるように整えていきます。

デザイナ編集中
最小限のハンドラを書きます。

private void _toolStripButtonClose_Click(object sender, EventArgs e)
{
    Close();
}

private void _toolStripButtonMaximize_Click(object sender, EventArgs e)
{
    WindowState = FormWindowState.Maximized;
}

private void _toolStripButtonMinimize_Click(object sender, EventArgs e)
{
    WindowState = FormWindowState.Minimized;
}

閉じる、最大化、最小化はできるようになりました。

ボタン追加

せっかくダークモードっぽくしたのに、マウスオーバー時のハイライトが残念なので調整します。

ハイライト色の微調整

ToolStripProfessionalRendererをカスタマイズします。

private class DarkThemeRenderer : ToolStripProfessionalRenderer
{
    protected override void OnRenderButtonBackground(ToolStripItemRenderEventArgs e)
    {
        if (!e.Item.Selected)
        {
            base.OnRenderButtonBackground(e);
        }
        else
        {
            Rectangle rectangle = new Rectangle(0, 0, e.Item.Size.Width - 1, e.Item.Size.Height - 1);
            e.Graphics.FillRectangle(SystemBrushes.ControlDark, rectangle);
            e.Graphics.DrawRectangle(SystemPens.ControlDark, rectangle);
        }
    }
}
_toolStripTitleBar.Renderer = new DarkThemeRenderer();

いい感じ

リサイズ・移動に対応する

システム操作が効かなくなっているので、この辺りも自前実装が必要になります。
が、ここができればあとは自由度の高いタイトルバーが扱えるようになるんじゃないかな。

リサイズに対応する

NativeMethodsにいくつかWinApiを追加します。

public const int WM_SYSCOMMAND = 0x0112;
public const int SC_MOVE = 0xF010;
public const int SC_SIZE = 0xF000;

[DllImport("User32.dll")]
public static extern int SendMessage(IntPtr hWnd, int uMsg, int wParam, int lParam);

[DllImport("User32.dll")]
public static extern bool SetCapture(IntPtr hWnd);

[DllImport("user32.dll")]
public static extern bool ReleaseCapture();

マウス操作のハンドラを追加します。
画面上部だけ反応しないので、そこだけ処理します。
また、ToolStripItemがのっかっている部分はそちらに処理が奪われてしまうのでItemにもハンドラを追加します。

protected void OnLoad(EventArgs e)
{
    base.OnLoad(e);
    foreach (ToolStripItem item in _toolStripTitleBar.Items)
    {
        if (item == null) continue;
        item.MouseDown += TitleBar_MouseDown;
        item.MouseMove += TitleBar_MouseMove;
    }
    _toolStripTitleBar.MouseMove += TitleBar_MouseMove;
    _toolStripTitleBar.MouseDown += TitleBar_MouseDown;
    _toolStripTitleBar.MouseDoubleClick += TitleBar_MouseDoubleClick;
}
private void TitleBar_MouseDown(object? sender, MouseEventArgs e)
{
    NativeMethods.SetCapture(_toolStripTitleBar.Handle);
    NativeMethods.ReleaseCapture();

    int flag = 0;
    if (e.Y < 5)
    {
        flag += 0x0003;
    }

    if (flag != 0)
        NativeMethods.SendMessage(Handle, NativeMethods.WM_SYSCOMMAND, NativeMethods.SC_SIZE | flag, 0);
}

private void TitleBar_MouseMove(object? sender, MouseEventArgs e)
{
    int flag = 0;
    if (e.Y < 5)
    {
        flag += 0x0003;
    }
    switch (flag)
    {
        case 0:
            Cursor = Cursors.Default;
            break;
        case 3:
            Cursor = Cursors.SizeNS;
            break;
    }
}

private void TitleBar_MouseDoubleClick(object? sender, MouseEventArgs e)
{
    ChangeWindowState();
}

private void ChangeWindowState()
{
    if (WindowState != FormWindowState.Maximized)
    {
        WindowState = FormWindowState.Maximized;
    }
    else
    {
        WindowState = FormWindowState.Normal;
    }
}

リサイズできるようになりました。

移動に対応する

上記のMouseDownのハンドラを以下のように変えたら動きました。

if (flag != 0)
{
    NativeMethods.SendMessage(Handle, NativeMethods.WM_SYSCOMMAND, NativeMethods.SC_SIZE | flag, 0);
}
else
{
    if (sender.Equals(_toolStripTitleBar))
    {
        //ダブルクリックと相性悪い状態だが未調整
        NativeMethods.SendMessage(Handle, NativeMethods.WM_SYSCOMMAND, NativeMethods.SC_MOVE | 2, 0);
    }
}

本当はダブルクリックした際に最大化する処理も入れたいのですが、調整が面倒だったので途中までの対応にしています。

出来上がり

見た目

こんな感じ

あとはToolStripに閉じてカスタマイズしたらよいし、LayoutPanel系と↓記事を組み合わせてWPFコントロールを使ったリッチタイトルバーとかも作れるようになります。

super-string.hatenablog.com

ソース

github.com

プライバシーポリシー


d払いポイントGETモール