none
WM_PAINTでTextBoxがちらつく RRS feed

  • 質問

  • Visual C# 2017
    .NET Framework 4.5
    WinForms

    TextBox を継承して、枠線やウォーターマークを表示させようとしています。
    それ自体は実現できたのですが、以下の点が不服であり、解消させようと調べてみましたが解決できませんでした。
     ・マウスポインタを行き来させるとTextBoxがちらつく(特にBorderStyle=Fixed3Dの時)
     ・BorderStyle=Fixed3Dの時、WM_PAINTが何度も走行するせいで、ウォーターマークの文字列が太くなる。

    ちらつき自体は妥協可能なレベルであれば構わないのですが、BorderStyle=Fixed3Dの時に限り、WM_PAINTが4, 5回走行する関係でちらつきもひどくて許容可能なレベルを超えています。
    なんとかする方法ありませんでしょうか?

    また、ウォーターマークの色を変更したいため、SendMessage()でEM_SETCUEBANNERを送信する手段はNGとしました。

    現象が発生を確認した最小コードは以下になります。

        class TextBoxEx : TextBox
        {
            private const int WM_PAINT = 0x000F;
    
            protected override void WndProc(ref Message m)
            {
                base.WndProc(ref m);
    
                if (m.Msg == WM_PAINT)
                {
                    
                    using (var g = this.CreateGraphics())
                    using (var pen = new Pen(Color.Red))
                    {
                        var rect = this.ClientRectangle;
    
                        // 枠線描画
                        g.DrawRectangle(pen, rect.X, rect.Y, rect.Width - 1, rect.Height - 1);
                        pen.Dispose();
    
                        // ウォーターマーク描画
                        g.DrawString("watermark", this.Font, new SolidBrush(Color.LightGray), rect.X, rect.Y, StringFormat.GenericTypographic);
                    }
                    return;
                }
            }
        }

    2018年10月31日 2:31

回答

  • 多くのコントロールがそうであるように、TextBox(EDIT)コントロールは、WM_PAINT の WPARAM 引数にデバイスコンテキストハンドル(HDC)を入れると、その HDC に対して描画を行います。

    具体的には、ビットマップ等のバッファを用意して、Graphics オブジェクトを作り、GetHdc メソッドで HDC を取得し base.WndProc を呼び出します。

    あとはビットマップの Graphics オブジェクトを加工したのち、コントロールの HDC に転送してやればよいです。

    コントロールの HDC は、BeginPaint で取得し EndPaint で後始末します。

    using System;
    using System.Runtime.InteropServices;
    using System.Drawing;
    using System.Windows.Forms;
    
    public class TextBoxEx : TextBox {
    
        private const int WM_PAINT = 0x000F;
    
        [StructLayout(LayoutKind.Sequential)]
        private struct PAINTSTRUCT {
            public IntPtr hdc;
            public bool fErase;
            public int rcPaint_left;
            public int rcPaint_top;
            public int rcPaint_right;
            public int rcPaint_bottom;
            public bool fRestore;
            public bool fIncUpdate;
            public int reserved1;
            public int reserved2;
            public int reserved3;
            public int reserved4;
            public int reserved5;
            public int reserved6;
            public int reserved7;
            public int reserved8;
        }
    
        [DllImport("user32.dll")]
        private static extern IntPtr BeginPaint(HandleRef hWnd, ref PAINTSTRUCT lpPaint);
    
        [DllImport("user32.dll")]
        private static extern bool EndPaint(HandleRef hWnd, ref PAINTSTRUCT lpPaint);
    
        protected override void WndProc(ref Message m) {
    
            switch (m.Msg) {
                case WM_PAINT:
                    using (var bmp = new Bitmap(ClientSize.Width, ClientSize.Height)) {
                        using (var bmpGraphics = Graphics.FromImage(bmp)) {
                            // bitmap に描画してもらう
                            var bmphdc = bmpGraphics.GetHdc();
                            var msg = Message.Create(m.HWnd, WM_PAINT, bmphdc, IntPtr.Zero);
                            base.WndProc(ref msg);
                            bmpGraphics.ReleaseHdc();
    
                            // ここで bmpGraphics に対して描画
    
                        }
                        // bitmap をコントロールに描画
                        if (m.WParam == IntPtr.Zero) {
                            var hWnd = new HandleRef(this, m.HWnd);
                            var ps = new PAINTSTRUCT();
                            var controlHdc = BeginPaint(hWnd, ref ps);
                            using (var controlGraphics = Graphics.FromHdc(controlHdc)) {
                                controlGraphics.DrawImage(bmp, 0, 0);
                            }
                            EndPaint(hWnd, ref ps);
                        } else {
                            using (var controlGraphics = Graphics.FromHdc(m.WParam)) {
                                controlGraphics.DrawImage(bmp, 0, 0);
                            }
                        }
    
                    }
                    break;
    
                default:
                    base.WndProc(ref m);
                    break;
            }
        }
    }
    

    パフォーマンスに難があるようでしたら、.NET のソースコード Control.cs の WmPaint を参考にすると良いかと思います。

    • 回答の候補に設定 kenjinoteMVP 2018年11月2日 12:33
    • 回答としてマーク takiru 2018年11月5日 0:52
    2018年10月31日 7:33

すべての返信

  • 多くのコントロールがそうであるように、TextBox(EDIT)コントロールは、WM_PAINT の WPARAM 引数にデバイスコンテキストハンドル(HDC)を入れると、その HDC に対して描画を行います。

    具体的には、ビットマップ等のバッファを用意して、Graphics オブジェクトを作り、GetHdc メソッドで HDC を取得し base.WndProc を呼び出します。

    あとはビットマップの Graphics オブジェクトを加工したのち、コントロールの HDC に転送してやればよいです。

    コントロールの HDC は、BeginPaint で取得し EndPaint で後始末します。

    using System;
    using System.Runtime.InteropServices;
    using System.Drawing;
    using System.Windows.Forms;
    
    public class TextBoxEx : TextBox {
    
        private const int WM_PAINT = 0x000F;
    
        [StructLayout(LayoutKind.Sequential)]
        private struct PAINTSTRUCT {
            public IntPtr hdc;
            public bool fErase;
            public int rcPaint_left;
            public int rcPaint_top;
            public int rcPaint_right;
            public int rcPaint_bottom;
            public bool fRestore;
            public bool fIncUpdate;
            public int reserved1;
            public int reserved2;
            public int reserved3;
            public int reserved4;
            public int reserved5;
            public int reserved6;
            public int reserved7;
            public int reserved8;
        }
    
        [DllImport("user32.dll")]
        private static extern IntPtr BeginPaint(HandleRef hWnd, ref PAINTSTRUCT lpPaint);
    
        [DllImport("user32.dll")]
        private static extern bool EndPaint(HandleRef hWnd, ref PAINTSTRUCT lpPaint);
    
        protected override void WndProc(ref Message m) {
    
            switch (m.Msg) {
                case WM_PAINT:
                    using (var bmp = new Bitmap(ClientSize.Width, ClientSize.Height)) {
                        using (var bmpGraphics = Graphics.FromImage(bmp)) {
                            // bitmap に描画してもらう
                            var bmphdc = bmpGraphics.GetHdc();
                            var msg = Message.Create(m.HWnd, WM_PAINT, bmphdc, IntPtr.Zero);
                            base.WndProc(ref msg);
                            bmpGraphics.ReleaseHdc();
    
                            // ここで bmpGraphics に対して描画
    
                        }
                        // bitmap をコントロールに描画
                        if (m.WParam == IntPtr.Zero) {
                            var hWnd = new HandleRef(this, m.HWnd);
                            var ps = new PAINTSTRUCT();
                            var controlHdc = BeginPaint(hWnd, ref ps);
                            using (var controlGraphics = Graphics.FromHdc(controlHdc)) {
                                controlGraphics.DrawImage(bmp, 0, 0);
                            }
                            EndPaint(hWnd, ref ps);
                        } else {
                            using (var controlGraphics = Graphics.FromHdc(m.WParam)) {
                                controlGraphics.DrawImage(bmp, 0, 0);
                            }
                        }
    
                    }
                    break;
    
                default:
                    base.WndProc(ref m);
                    break;
            }
        }
    }
    

    パフォーマンスに難があるようでしたら、.NET のソースコード Control.cs の WmPaint を参考にすると良いかと思います。

    • 回答の候補に設定 kenjinoteMVP 2018年11月2日 12:33
    • 回答としてマーク takiru 2018年11月5日 0:52
    2018年10月31日 7:33
  • 教えていただいたコードを参考に、色々動かしてみたのですが、以下のコードは、具体的にどういった操作を行った時に発生するでしょうか?

                        } else {
                            using (var controlGraphics = Graphics.FromHdc(m.WParam)) {
                                controlGraphics.DrawImage(bmp, 0, 0);
                            }
                        }

    最大化、最小化、ウィンドウを画面端に追いやりなどを行ってみましたが、到達しませんでした。

    2018年11月1日 4:55
  • 「WM_PAINT の WPARAM 引数に HDC を入れると、それに対して描画を行う。」というコントロールの仕様を満足するための処理です。

    通常の操作では発生しないと思います。

    強いてあげるなら、TextBoxEx をさらに継承して描画をカスタマイズするときに使えるか・・・
    と思いましたが、イベントやメソッドを作成したほうが親切ですね。

    余力があれば、WM_PRINTCLIENT も同様の処理をしておくと良いです。
    こちらは DrawToBitmap メソッドを実行したときに呼ばれます。



    • 編集済み KOZ6.0 2018年11月1日 8:37
    2018年11月1日 6:23
  • 遅くなりましたが、それぞれのコードを理解し、望んだ動作をすることができました。
    ありがとうございました。
    2018年11月5日 0:52