在開發 Windows Forms 應用程式時,PrintPreviewControl 控制項常用於顯示列印預覽畫面。雖然該控制項內建水平與垂直捲軸,但使用者習慣像看圖軟體那樣,直接按住滑鼠左鍵拖曳頁面來瀏覽超出可視範圍的內容。可惜 .NET Framework 並未直接提供這項功能,我們必須自行實作。
由於 PrintPreviewControl 未公開捲軸相關的方法或屬性,我們只能求助於 Windows API。關鍵在於呼叫 SetScrollPos 設定捲軸位置,再透過 PostMessage 發送捲動訊息(WM_HSCROLL / WM_VSCROLL),讓控制項真正重繪內容。若缺少 PostMessage,捲軸雖會移動,但頁面內容卻不會跟著更新——這是許多開發者容易踩的坑。
[DllImport("user32.dll")]
private static extern int SetScrollPos(IntPtr hwnd, int nBar, int nPos, bool bRedraw);
[DllImport("user32.dll")]
private static extern int GetScrollPos(IntPtr hwnd, int nBar);
[DllImport("user32.dll")]
private static extern bool PostMessage(IntPtr hWnd, int Msg, int wParam, int lParam);
[DllImport("user32", CharSet = CharSet.Auto)]
private static extern bool GetScrollRange(IntPtr hWnd, int nBar, out int lpMinPos, out int lpMaxPos);
private const int SB_HORZ = 0x0;
private const int SB_VERT = 0x1;
private const int WM_HSCROLL = 0x114;
private const int WM_VSCROLL = 0x115;
private const int SB_THUMBPOSITION = 4;
bool isDragging = false; // 是否正在拖曳
Point dragStart; // 滑鼠按下時的座標
Point dragCurrent; // 滑鼠移動時的目前座標
private void previewer_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
isDragging = true;
dragStart = e.Location;
previewer.Cursor = Cursors.SizeAll; // 視覺回饋
}
}
private void previewer_MouseMove(object sender, MouseEventArgs e)
{
if (!isDragging) return;
dragCurrent = e.Location;
// 位移量除以 10 降低移動速度,負號使頁面跟隨滑鼠方向移動
int deltaX = -(dragCurrent.X - dragStart.X) / 10;
int deltaY = -(dragCurrent.Y - dragStart.Y) / 10;
// 取得捲軸範圍與目前位置
GetScrollRange(previewer.Handle, SB_HORZ, out int minH, out int maxH);
GetScrollRange(previewer.Handle, SB_VERT, out int minV, out int maxV);
int posH = GetScrollPos(previewer.Handle, SB_HORZ);
int posV = GetScrollPos(previewer.Handle, SB_VERT);
// 計算新位置並邊界檢查
int newH = posH + deltaX;
if (newH >= minH && newH <= maxH)
{
SetScrollPos(previewer.Handle, SB_HORZ, newH, true);
// wParam: SB_THUMBPOSITION | (newH << 16)
PostMessage(previewer.Handle, WM_HSCROLL, SB_THUMBPOSITION + 0x10000 * newH, 0);
}
int newV = posV + deltaY;
if (newV >= minV && newV <= maxV)
{
SetScrollPos(previewer.Handle, SB_VERT, newV, true);
PostMessage(previewer.Handle, WM_VSCROLL, SB_THUMBPOSITION + 0x10000 * newV, 0);
}
}
private void previewer_MouseUp(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
isDragging = false;
previewer.Cursor = Cursors.Default;
}
}
PostMessage 的 wParam 高字組為捲軸新位置,低字組為捲動類型(SB_THUMBPOSITION 表示拖曳滑塊到指定位置)。Panel、PictureBox 等)。WM_MOUSEWHEEL 處理滾輪縮放,或加入雙緩衝減少閃爍。完整專案原始碼可參考:推箱子專案原始碼(內含此拖曳邏輯的實作)。