ウィンドウコントロールのスクロールバーでScrollWindowEx APIを利用して内部のコンテンツ領域をスクロールする

ウィンドウコントロールのスクロールバーで内部のコンテンツ領域をスクロールするコードを紹介します。
スクロールの処理にScrollWindowEx APIを利用します。

概要

こちらの記事では ウィンドウコントロールのスクロールバーで内部のコンテンツ領域をスクロールするコードを紹介しました。 紹介したコードでコンテンツのスクロールはできますが、スクロール時にちらつきが多くなってしまいます。 これは、スクロールごとにコントロールの画面全体を再描画するためです。
この記事では、こちらの記事で紹介しているScrollWindowEx Windows APIを利用し、 スクロールインした部分のみを描画することで、 スクロール時のちらつきを抑える実装を紹介します。

プログラム

Windows Formアプリケーションを作成します。

コード

コンポーネントを作成し、以下のコードを記述します。

MyComponent.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace WindowControlScroll
{
  public partial class MyComponent : Control
  {
    // window style constants for scrollbars
    public const int WS_VSCROLL = 0x00200000;
    public const int WS_HSCROLL = 0x00100000;

    public const int WM_LBUTTONDOWN = 0x00000201;
    public const int WM_RBUTTONDOWN = 0x00000204;

    public const int WM_HSCROLL = 0x00000114;
    public const int WM_VSCROLL = 0x00000115;

    /*
     * Scroll Bar Commands
     */
    public const int SB_LINEUP = 0;
    public const int SB_LINELEFT = 0;
    public const int SB_LINEDOWN = 1;
    public const int SB_LINERIGHT = 1;
    public const int SB_PAGEUP = 2;
    public const int SB_PAGELEFT = 2;
    public const int SB_PAGEDOWN = 3;
    public const int SB_PAGERIGHT = 3;
    public const int SB_THUMBPOSITION = 4;
    public const int SB_THUMBTRACK = 5;
    public const int SB_TOP = 6;
    public const int SB_LEFT = 6;
    public const int SB_BOTTOM = 7;
    public const int SB_RIGHT = 7;
    public const int SB_ENDSCROLL = 8;

    [StructLayout(LayoutKind.Sequential)]
    struct SCROLLINFO
    {
      public uint cbSize;
      public uint fMask;
      public int nMin;
      public int nMax;
      public uint nPage;
      public int nPos;
      public int nTrackPos;
    }

    private enum ScrollBarDirection
    {
      SB_HORZ = 0,
      SB_VERT = 1,
      SB_CTL = 2,
      SB_BOTH = 3
    }

    private enum ScrollInfoMask
    {
      SIF_RANGE = 0x0001,
      SIF_PAGE = 0x0002,
      SIF_POS = 0x0004,
      SIF_DISABLENOSCROLL = 0x0008,
      SIF_TRACKPOS = 0x0010,
      SIF_ALL = SIF_RANGE | SIF_PAGE | SIF_POS | SIF_TRACKPOS
      //SIF_ALL = SIF_RANGE + SIF_PAGE + SIF_POS + SIF_TRACKPOS
    }


    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool GetScrollInfo(IntPtr hwnd, int fnBar, ref SCROLLINFO lpsi);

    [DllImport("user32.dll")]
    private static extern int SetScrollInfo(IntPtr hwnd, int fnBar, [In] ref SCROLLINFO lpsi, bool fRedraw);


    public const uint SW_SCROLLCHILDREN = 0x0001;
    public const uint SW_INVALIDATE = 0x0002;
    public const uint SW_ERASE = 0x0004;
    public const uint SW_SMOOTHSCROLL = 0x0010;

    [StructLayout(LayoutKind.Sequential)]
    public struct RECT
    {
      public int _Left;
      public int _Top;
      public int _Right;
      public int _Bottom;
    }


    [DllImport("user32.dll")]
    static extern int ScrollWindowEx(IntPtr hWnd, int dx, int dy, ref RECT prcScroll, ref RECT prcClip, IntPtr hrgnUpdate, out RECT prcUpdate, uint flags);

    const int ScrollOffset = 2;
    public Point sc = new Point(0, 0);

    public MyComponent()
    {
      InitializeComponent();
    }

    public MyComponent(IContainer container)
    {
      container.Add(this);

      InitializeComponent();
    }

    protected override void InitLayout()
    {
      base.InitLayout();
      int tp;
      SetVScrollBar(sc.Y, (uint)ClientSize.Height, 0, (64 * 12) + ScrollOffset, out tp);
      SetHScrollBar(sc.X, (uint)ClientSize.Width, 0, (64 * 12) + ScrollOffset, out tp);
    }

    protected override CreateParams CreateParams {
      get
      {
        CreateParams cp = base.CreateParams;
        cp.Style |= WS_HSCROLL + WS_VSCROLL;

        return cp;
      }
    }

    protected override void WndProc(ref Message message)
    {
      base.WndProc(ref message);
      short lo;
      int tp;
      Point org_sc;

      switch (message.Msg) {
        case WM_HSCROLL:
          org_sc = new Point(sc.X, sc.Y);
          lo = LoWord((long)message.WParam);
          switch (lo) {
            case SB_ENDSCROLL:
              break;
            case SB_LEFT:
              break;
            case SB_RIGHT:
              break;
            case SB_LINELEFT:
              sc.X = Math.Max(sc.X -= 8, 0);
              break;
            case SB_LINERIGHT:
              sc.X = Math.Min(sc.X += 8, (64 * 12) - ClientRectangle.Width + ScrollOffset);
              break;
            case SB_PAGELEFT:
              sc.X = Math.Max(sc.X -= 32, 0);
              break;
            case SB_PAGERIGHT:
              sc.X = Math.Min(sc.X += 32, (64 * 12) - ClientRectangle.Width + ScrollOffset);
              break;
            case SB_THUMBPOSITION:
              break;
            case SB_THUMBTRACK:
              SCROLLINFO HScrInfo = new SCROLLINFO();
              HScrInfo.fMask = (uint)ScrollInfoMask.SIF_TRACKPOS;
              GetScrollInfo(this.Handle, (int)ScrollBarDirection.SB_HORZ, ref HScrInfo);
              sc.X = HScrInfo.nTrackPos;
              break;
          }

          SetHScrollBar(sc.X, (uint)ClientSize.Width, 0, (64 * 12) + ScrollOffset, out tp);
          ClientScroll(org_sc.X - sc.X, org_sc.Y - sc.Y);
          break;
        case WM_VSCROLL:
          org_sc = new Point(sc.X, sc.Y);
          lo = LoWord((long)message.WParam);
          switch (lo) {
            case SB_BOTTOM:
              break;
            case SB_ENDSCROLL:
              break;
            case SB_LINEDOWN:
              sc.Y = Math.Min(sc.Y += 8, (64 * 12) - ClientRectangle.Height+ ScrollOffset);
              break;
            case SB_LINEUP:
              sc.Y = Math.Max(sc.Y -= 8, 0);
              break;
            case SB_PAGEDOWN:
              sc.Y = Math.Min(sc.Y += 32, (64 * 12) - ClientRectangle.Height + ScrollOffset);
              break;
            case SB_PAGEUP:
              sc.Y = Math.Max(sc.Y -= 32, 0);
              break;
            case SB_THUMBPOSITION:
              break;
            case SB_THUMBTRACK:
              SCROLLINFO VScrInfo = new SCROLLINFO();
              VScrInfo.fMask = (uint)ScrollInfoMask.SIF_TRACKPOS;
              GetScrollInfo(this.Handle, (int)ScrollBarDirection.SB_VERT, ref VScrInfo);
              sc.Y = VScrInfo.nTrackPos;
              break;
            case SB_TOP:
              break;
          }
          SetVScrollBar(sc.Y, (uint)ClientSize.Height, 0, (64 * 12) + ScrollOffset, out tp);
          ClientScroll(org_sc.X- sc.X, org_sc.Y - sc.Y);
          break;
      }
    }

    protected override void OnResize(EventArgs e)
    {
      base.OnResize(e);
      int tp;
      SetVScrollBar(sc.Y, (uint)ClientSize.Height, 0, (64 * 12) + ScrollOffset, out tp);
      SetHScrollBar(sc.X, (uint)ClientSize.Width, 0, (64 * 12) + ScrollOffset, out tp);
    }

    protected override void OnPaint(PaintEventArgs e)
    {
      int rWidth = 64;
      int rHeiht = 64;

      Pen p = new Pen(Color.FromArgb(0x5d, 0x86, 0xba), 1);
      SolidBrush b = new SolidBrush(Color.FromArgb(0xcd, 0xdc, 0xef));

      Font f = new Font("MS UI Gothic", 10);
      SolidBrush bs = new SolidBrush(Color.FromArgb(0x00, 0x44, 0x99));

      int idx = 0;
      for (int y = 0; y < 12; y++) {
        for (int x = 0; x < 12; x++) {
          Rectangle drawRect = new Rectangle(x * rWidth - sc.X, y * rHeiht - sc.Y, rWidth, rHeiht);
          if (e.ClipRectangle.IntersectsWith(drawRect) == true) {

            e.Graphics.FillRectangle(b, drawRect);
            e.Graphics.DrawRectangle(p, drawRect);

            e.Graphics.DrawString(Convert.ToString(idx), f, bs, new Point(x * rWidth - sc.X + 2, y * rHeiht - sc.Y + 2));
          }
          idx++;
        }
      }
    }

    protected short LoWord(long input)
    {
      return (short)((int)input & 0xFFFF);
    }

    protected short HiWord(long input)
    {
      return (short)((int)input >> 16);
    }

    public void SetVScrollBar(int Position, uint Page, int nMin, int nMax, out int TrackPosition)
    {
      SCROLLINFO VScrInfo = new SCROLLINFO();
      GetScrollInfo(this.Handle, (int)ScrollBarDirection.SB_VERT, ref VScrInfo);
      VScrInfo.cbSize = (uint)Marshal.SizeOf(VScrInfo);
      VScrInfo.nPos = Position;
      VScrInfo.nPage = Page;
      VScrInfo.nMin = nMin;
      VScrInfo.nMax = nMax;
      TrackPosition = VScrInfo.nTrackPos;

      VScrInfo.fMask = (int)ScrollInfoMask.SIF_POS + (int)ScrollInfoMask.SIF_PAGE + (int)ScrollInfoMask.SIF_RANGE + (int)ScrollInfoMask.SIF_DISABLENOSCROLL;
      SetScrollInfo(this.Handle, (int)ScrollBarDirection.SB_VERT, ref VScrInfo, true);
    }

    public void SetHScrollBar(int Position, uint Page, int nMin, int nMax, out int TrackPosition)
    {
      SCROLLINFO HScrInfo = new SCROLLINFO();
      GetScrollInfo(this.Handle, (int)ScrollBarDirection.SB_VERT, ref HScrInfo);
      HScrInfo.cbSize = (uint)Marshal.SizeOf(HScrInfo);
      HScrInfo.nPos = Position;
      HScrInfo.nPage = Page;
      HScrInfo.nMin = nMin;
      HScrInfo.nMax = nMax;
      TrackPosition = HScrInfo.nTrackPos;

      HScrInfo.fMask = (int)ScrollInfoMask.SIF_POS + (int)ScrollInfoMask.SIF_PAGE + (int)ScrollInfoMask.SIF_RANGE + (int)ScrollInfoMask.SIF_DISABLENOSCROLL;
      SetScrollInfo(this.Handle, (int)ScrollBarDirection.SB_HORZ, ref HScrInfo, true);
    }

    private void ClientScroll(int x, int y)
    {
      if (x == 0 && y == 0) return;

      RECT rect = new RECT();
      rect._Left = 0;
      rect._Top = 0;
      rect._Right = Width;
      rect._Bottom = Height;
      RECT clip = new RECT();
      clip._Left = 0;
      clip._Top = 0;
      clip._Right = Width;
      clip._Bottom = Height;
      RECT udaterect = new RECT();
      ScrollWindowEx(Handle, x, y, ref rect, ref clip, (IntPtr)null, out udaterect, SW_SCROLLCHILDREN);

      Invalidate(new Rectangle(udaterect._Left, udaterect._Top, udaterect._Right - udaterect._Left, udaterect._Bottom - udaterect._Top));
    }

  }
}

UI

下図のフォームを作成します。作成したコンポーネントをフォームに配置しています。

ウィンドウコントロールのスクロールバーでScrollWindowEx APIを利用して内部のコンテンツ領域をスクロールする:画像1

解説

スクロールバーの設定や処理のコードの詳細はこちらの記事を参照してください。

ScrollWindowExによるスクロール部分

ScrollWindowEx Windows APIで描画内容をスクロールするメソッドを実装します。 ClientScroll() メソッドのパラメーターに与えたピクセル数ウィンドウの描画内容をスクロールします。
ScrollWindowExによるスクロールの動作の詳細はこちらの記事を参照して下さい。
スクロール後に、Invalidate() メソッドを呼び出し、スクロールインした領域を描画します。

    private void ClientScroll(int x, int y)
    {
      if (x == 0 && y == 0) return;

      RECT rect = new RECT();
      rect._Left = 0;
      rect._Top = 0;
      rect._Right = Width;
      rect._Bottom = Height;
      RECT clip = new RECT();
      clip._Left = 0;
      clip._Top = 0;
      clip._Right = Width;
      clip._Bottom = Height;
      RECT udaterect = new RECT();
      ScrollWindowEx(Handle, x, y, ref rect, ref clip, (IntPtr)null, out udaterect, SW_SCROLLCHILDREN);

      Invalidate(new Rectangle(udaterect._Left, udaterect._Top, udaterect._Right - udaterect._Left, udaterect._Bottom - udaterect._Top));
    }


WndProcメソッドをオーバーライドして、スクロールに関するメッセージを受け取った場合の処理を実装します。
スクロールバーが操作された場合は、スクロール量を計算し、ClientScroll() メソッドを呼び出し描画領域をスクロールします。
スクロール量は、スクロールバーの可動範囲を超えた場合に値を抑える処理や、スクロール位置がマイナスにならないよう値を抑える処理があるため、 前回のスクロール位置から、スクロール後のスクロール位置を引いた値をスクロール量として計算しています。

  switch (message.Msg) {
    case WM_HSCROLL:
      /* 中略 */
      SetHScrollBar(sc.X, (uint)ClientSize.Width, 0, (64 * 12) + ScrollOffset, out tp);
      ClientScroll(org_sc.X - sc.X, org_sc.Y - sc.Y);
      break;
    case WM_VSCROLL:
      /* 中略 */
      SetVScrollBar(sc.Y, (uint)ClientSize.Height, 0, (64 * 12) + ScrollOffset, out tp);
      ClientScroll(org_sc.X- sc.X, org_sc.Y - sc.Y);
      break;
  }

実行結果

プロジェクトを実行します。下図のウィンドウが表示されます。
ウィンドウコントロールのスクロールバーでScrollWindowEx APIを利用して内部のコンテンツ領域をスクロールする:画像2

スクロールバーをクリックします。描画内容がスクロールする動作が確認できます。
ウィンドウコントロールのスクロールバーでScrollWindowEx APIを利用して内部のコンテンツ領域をスクロールする:画像3 ウィンドウコントロールのスクロールバーでScrollWindowEx APIを利用して内部のコンテンツ領域をスクロールする:画像4

ウィンドウサイズを変更します。スクロールバーのスライダーのサイズがクライアント領域のサイズに合わせて変化する様子が確認できます。
ウィンドウコントロールのスクロールバーでScrollWindowEx APIを利用して内部のコンテンツ領域をスクロールする:画像5

コンテンツがすべて見える大きさまでウィンドウを広げるとスクロールバーが灰色になり無効になります。
ウィンドウコントロールのスクロールバーでScrollWindowEx APIを利用して内部のコンテンツ領域をスクロールする:画像6

動画の実行結果です。こちらの記事の実行結果と比べて、ちらつきが無いことが確認できます。

AuthorPortraitAlt
著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用
作成日: 2023-02-13
Copyright © 1995–2025 iPentec all rights reserverd.