主要なカテゴリのまとめページへのリンクです。最近は更新をさぼり気味です……
C#&Win API MDI子フォームのStatusStripにサイズグリップを強制表示する。
以前の投稿でMDI子フォームはMDI親フォームの最大化時にサイズグリップが非表示になるWinFormsの仕様を紹介しました。この記事ではウィンドウメッセージとToolStripRenderer
を利用した強制表示を紹介します。動作確認環境は.NET 8.0ですが、他のバージョンでも変わらないと思います。
使用する技術は次の3つです。
StatusStrip
派生コントロールの作成。WndProc
のオーバーロードによるWM_NCHITTEST
処理の上書き。OnPaint
のオーバーロードとToolStripRenderer
によるサイズグリップの描画。
暗黙的な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 APIの
GetAncestor
関数でルートウィンドウの最大化状態を参照している。 - 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
等はこの機能でサブクラス化している。
参考
- WinForms - GihHub (Microsoft)(変な操作を防ぐためにアクセス時GitHubログアウト推奨)
C++20 std::views::splitはsubrange型を返す。
std::views::split
の結果をそのままto<std::vector>
に渡してコンパイルエラーに悩まされたのでメモとして。
結論としてstd::views::split
の結果をstd::ranges::to
でSTLコンテナに変換するとき、手前でstd::wstring
やstd::wstring_view
に変換する必要があります。std::views::split
の結果はstd::ranges::subrange
型だからです。
下記にサンプルコードを示します。丸ごと補助関数にまとめたり、std::wstring
やstd::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_view
のclass _Iterator::value_type
でsubrange
が明示されているので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 ファイルの新規作成登録の注意点
メモ帳をアンインストールしたらファイルの新規作成からテキストファイルが消えました。修復過程で気付いた注意点を共有します。
- 「新規作成」項目の実体は拡張子単位のレジストリキー
ShellNew
。HKCR
直下の拡張子キーまたは拡張子キー直下のファイル型キーに作成する。 - ファイル型キーは拡張子キーの既定の値と同名のレジストリキーで通常は
HKCR
直下だが、新規作成で参照されるShellNew
キーは特別扱い。ShellNew
キーは拡張子キー直下のファイル型キーに登録できるが、新規作成時の項目名はHKCR
直下のファイル型キーの既定値が使われる。 - 拡張子キーまたはその直下のファイル型キーに
ShellNew
キーを作成しても、HKCR
直下のファイル型キーの既定値が空白だと新規作成に表示されない。ShellNew
キーにItemName
値を登録すれば表示されるかもしれない。ただし、ItemName
値はPEファイルのリソース位置のみ。
Windows11でストアアプリ版のメモ帳をアンインストールすると「.txt
」は「txtfilelegacy
」に関連付けられました。HKCR\txtfilelegacy
キーは既定値が空白なので、HKCR\.txt
キーまたはHKCR\.txt\txtfilelegacy
キーにShellNew
キーを作成しても上記の理由から新規作成に追加されません。HKCR\.txt\txtfilelegacy
キーの既定値を「テキストファイル」に変更すれば解決しました。