potisanのプログラミングメモ

趣味のプログラマーがプログラミング関係で気になったことや調べたことをいつでも忘れられるようにメモするブログです。はてなブログ無料版なので記事の上の方はたぶん広告です。記事中にも広告挿入されるみたいです。

C#&Win API MDI子フォームのStatusStripにサイズグリップを強制表示する。

以前の投稿でMDI子フォームはMDI親フォームの最大化時にサイズグリップが非表示になるWinFormsの仕様を紹介しました。この記事ではウィンドウメッセージとToolStripRendererを利用した強制表示を紹介します。動作確認環境は.NET 8.0ですが、他のバージョンでも変わらないと思います。

使用する技術は次の3つです。

暗黙的なglobal usingを使った具体的なコードは以下の通りです。とりあえず機能することを目的としています。プロジェクトで使用するには次のコードを適用なCSファイルに貼り付け、目的とするフォームの「*.Designer.cs」でStatusStripをStatusStripWithSizeGripに置き換えてください。

namespace Utility.Windows.Forms; // TODO:適当な名前空間に変更

internal sealed class StatusStripWithSizeGrip : StatusStrip
{
    protected override void OnCreateControl()
    {
        base.OnCreateControl();

        // SizeGripBoundsを使いたいのでサイズグリップを強制表示します。
        // この後の変更は対応しません。
        SizingGrip = true;
    }

    protected override void WndProc(ref Message m)
    {
        const int WM_NCHITTEST = 0x0084;
        const int HTBOTTOMRIGHT = 17;

        if (m.Msg == WM_NCHITTEST)
        {
            var pt = PointToClient(Cursor.Position);
            if (SizeGripBounds.Contains(pt))
            {
                m.Result = HTBOTTOMRIGHT;
                return;
            }
        }

        base.WndProc(ref m);
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);

        // 直接の親フォームが最大化されていなければサイズグリップを描画
        if (Parent is Form form && form.WindowState == FormWindowState.Maximized)
            return;

        // サイズグリップを描画する。描画済みでも上書き。
        Renderer.DrawStatusStripSizingGrip(new(e.Graphics, this));
    }
}

C#&WinForms MDI子フォームのStatusStripでサイズグリップが消える。

追伸:解決方法を別の記事に記載しました。

WinFormsでMDI子フォームにStatusStripを置くとMDI親フォームの最大化時にサイズグリップが消えます。GitHub上のソースコードを調べると設計上の仕様です。具体的には次の通りです。

  • StatusStripはサイズグリップの描画判定時、Win APIGetAncestor関数でルートウィンドウの最大化状態を参照している。
  • MDI子フォームはMDI親フォーム内に存在するため、上記のルートウィンドウはMDI親フォームになる。なのでMDI親フォームが最大化されていればサイズグリップは非表示になる。
  • この仕様はおそらくStatusStripをフォーム以外にも配置できるようにした結果&設計思想。実際には親フォームがMDI子フォームかは順に辿って判定できるが、おそらく判定のコストから実施していない。あとMDIウィンドウは旧式扱いらしいので。

解決策は以下のどれかだと思います。どれも試してはいません。

  • StatusStripのサイズグリップを諦める。
  • StatusStripを使わず、Formのサイズグリップを使う。
  • StatusStripを使うが、Dock指定やコンテナ使用でサイズグリップ分の隙間を空けてフォーム自体のサイズグリップを表示する。
  • StatusStripを使わず、TextBoxやListViewの上下・左右スクロールバーを気合いで表示させてサイズグリップも描画させる。
  • StatusStrip派生クラスを作成してサイズグリップ描画や使用処理を上書きする。

C# WinFormsのクリップボード監視コンポーネント

WinFormsのFormへのクリップボード監視機能の追加、コンポーネントNativeWindowで思ったより簡単に実装できました。NativeWindowの理解が浅いので問題が残っているかもしれませんが、とりあえず動きはします。動作確認はC# (.NET 8.0)ですが、名前空間などの書き方を変えれば以前の.NETでも使えると思います。

プロジェクトにClipboardWatcher.cs等の名前で次のコードを追加して一度ビルドします。後はツールボックスからFormに追加できます。

#pragma warning disable SYSLIB1054

using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;

namespace Utility.Windows.Forms;

/// <summary>
/// クリップボードの変更を監視します。
/// </summary>
[SupportedOSPlatform("windows")]
[DefaultEvent(nameof(ClipboardUpdated))]
internal sealed class ClipboardWatcher : Component
{
    /// <summary>
    /// クリップボードが変更されました。
    /// </summary>
    public event EventHandler? ClipboardUpdated;

    private sealed class ClipboardWatcherNativeWindow : NativeWindow
    {
        private readonly ClipboardWatcher _owner;
        public ClipboardWatcherNativeWindow(ClipboardWatcher owner)
        {
            _owner = owner;
            CreateHandle(new CreateParams());
        }

        protected override void WndProc(ref Message m)
        {
            const int WM_CLIPBOARDUPDATE = 0x031D;

            switch (m.Msg)
            {
            case WM_CLIPBOARDUPDATE:
                _owner.OnClipboardUpdated(_owner, new());
                break;
            }

            base.WndProc(ref m);
        }

        protected override void OnHandleChange()
        {
            base.OnHandleChange();
            NativeMethods.AddClipboardFormatListener(Handle);
        }

        public override void ReleaseHandle()
        {
            NativeMethods.RemoveClipboardFormatListener(Handle);
            base.ReleaseHandle();
        }

        private static class NativeMethods
        {
            [DllImport("user32.dll")]
            [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public extern static bool AddClipboardFormatListener(nint hwnd);
            [DllImport("user32.dll")]
            [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public extern static bool RemoveClipboardFormatListener(nint hwnd);
        }
    }

    private ClipboardWatcherNativeWindow? _native;

    public ClipboardWatcher()
    {
        if (!DesignMode)
            _native = new(this);
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _native?.ReleaseHandle();
            _native = null;
        }

        base.Dispose(disposing);
    }

    /// <summary>
    /// <code>ClipboardUpdated</code>イベントを呼び出すかを取得または設定します。
    /// </summary>
    [DefaultValue(true)]
    [Category("Behavior")]
    public bool Enabled { get; set; } = true;

    private void OnClipboardUpdated(object? sender, EventArgs e)
    {
        if (Enabled)
            ClipboardUpdated?.Invoke(sender, e);
    }
}

作成中に学んだこと。

  • 低レベルのWindowsネイティブウィンドウ操作はNativeWindowクラスの派生クラスを作成する。
  • NativeWindowは自動でウィンドウを作らないので、コンストラクタなどでCreateHandleを呼び出す。
  • CreateHandleの引数でクラス名を指定するとサブクラス化ウィンドウが作成できる。System.Windows.Forms.Button等はこの機能でサブクラス化している。

参考

C++20 std::views::splitはsubrange型を返す。

std::views::splitの結果をそのままto<std::vector>に渡してコンパイルエラーに悩まされたのでメモとして。

結論としてstd::views::splitの結果をstd::ranges::toSTLコンテナに変換するとき、手前でstd::wstringstd::wstring_viewに変換する必要があります。std::views::splitの結果はstd::ranges::subrange型だからです。

下記にサンプルコードを示します。丸ごと補助関数にまとめたり、std::wstringstd::wstring_viewへの変換だけ補助関数にしても良いかもしれません。

#include <ranges>
#include <string>
#include <vector>
#include <iostream>

using namespace std::string_view_literals;

int wmain()
{
    auto s1{ L"ABC\0DEF\0GHI\0\0"sv };

    auto sv1{ std::views::split(s1, L'\0')
        | std::views::transform([](const auto& sr) {return std::wstring(std::ranges::begin(sr), std::ranges::end(sr)); })
        | std::ranges::to<std::vector>() };

    auto svv1{ std::views::split(s1, L'\0')
        | std::views::transform([](const auto& sr) {return std::wstring_view(sr); })
        | std::ranges::to<std::vector>() };

    return -1;
}

std::views::splitの動作は次のコードで確認できます。MSVC 2022であればstd::ranges::split_viewclass _Iterator::value_typesubrangeが明示されているのでSTLソースコードからも確認できます。

#include <ranges>
#include <string>
#include <vector>
#include <iostream>

using namespace std::string_view_literals;

int wmain()
{
    auto s1{ L"ABC\0DEF\0GHI\0\0"sv };

    for (const auto& i : std::views::split(s1, L'\0'))
    {
        // iはstd::ranges::subrange型です。
        // const std::ranges::subrange<std::_String_view_iterator<std::char_traits<wchar_t>>,std::_String_view_iterator<std::char_traits<wchar_t>>,1> &

        // デバッグウィンドウに表示するための操作
        auto j = i;
    }

    for (const auto& i : std::views::split(s1, L'\0'))
    {
        // std::ranges::subrangeはレンジなのでstd::wstring_viewに変換できます。
        std::wstring_view sv(i);
        // std::wstringはレンジからの作成に未対応なのでエラーになります。
        //std::wstring sv(i);
    }

    // 以下をコメントアウトするとコンパイルエラーになります。
    // std::views::splitの戻り値型std::ranges::subrangeはstd::vectorの要素にできません。
    // 下記のようにtransformでstd::wstring_viewやstd::wstringへ変換すれば解決します。
    //for (const auto& i : std::views::split(s1, L'\0') | std::ranges::to<std::vector>())
    //{
    // std::wstring_view sv(i);
    //}

    // std::wstringはレンジからの作成に未対応です。イテレータペアから作成します。
    auto sv1{ std::views::split(s1, L'\0')
        | std::views::transform([](const auto& sr) {return std::wstring(std::ranges::begin(sr), std::ranges::end(sr)); })
        | std::ranges::to<std::vector>() };

    // std::wstring_viewはレンジからの作成に対応しています。記述量が減ります。
    // ただし、元の文字列がリテラルでない場合はメモリが解放されると無効になります。
    auto svv1{ std::views::split(s1, L'\0')
        | std::views::transform([](const auto& sr) {return std::wstring_view(sr); })
        | std::ranges::to<std::vector>() };

    return -1;
}

参考

Windows11 ファイルの新規作成登録の注意点

メモ帳をアンインストールしたらファイルの新規作成からテキストファイルが消えました。修復過程で気付いた注意点を共有します。

  • 「新規作成」項目の実体は拡張子単位のレジストリキーShellNewHKCR直下の拡張子キーまたは拡張子キー直下のファイル型キーに作成する。
  • ファイル型キーは拡張子キーの既定の値と同名のレジストリキーで通常はHKCR直下だが、新規作成で参照されるShellNewキーは特別扱い。ShellNewキーは拡張子キー直下のファイル型キーに登録できるが、新規作成時の項目名はHKCR直下のファイル型キーの既定値が使われる。
  • 拡張子キーまたはその直下のファイル型キーにShellNewキーを作成しても、HKCR直下のファイル型キーの既定値が空白だと新規作成に表示されない。
    • ShellNewキーにItemName値を登録すれば表示されるかもしれない。ただし、ItemName値はPEファイルのリソース位置のみ。

Windows11でストアアプリ版のメモ帳をアンインストールすると「.txt」は「txtfilelegacy」に関連付けられました。HKCR\txtfilelegacyキーは既定値が空白なので、HKCR\.txtキーまたはHKCR\.txt\txtfilelegacyキーにShellNewキーを作成しても上記の理由から新規作成に追加されません。HKCR\.txt\txtfilelegacyキーの既定値を「テキストファイル」に変更すれば解決しました。