Разделительная полоска (сплиттер)

Перевод А. И. Легалова

Англоязычный оригинал находится на сервере компании

Разделительная полоска — полезный элемент, который не входит в состав элементову правления Windows. Насколько трудной его реализовать? Не столь трудно, как это кажется, если Вы знаете хотя бы основы Windows API. Представленное описание может в начале показаться сложным, но вы изучите несколько очень важных методов, которые могут многократно использоваться в различных местах. Работа с дочерними окнами, сбор данных от мыши, рисование с использованием xor (исключающего или) режима — вот только некоторые из них.

Сплиттер — это окно. Точнее — это дочернее окно. Оно позиционировано между двумя другими дочерними окнами — мы назовем их левым и правым подокном, соответственно (или верхним и нижним для горизонтального расщепителя). Должно также быть основное окно, явялющееся родителем для трех дочерних.

Без дальнейшей суеты приведем код WinMain, который осуществляет начальные установки.

// Create top window class

TopWinClassMaker topWinClass(WndProcMain, ID_MAIN, hInst, ID_MAIN);

topWinClass.Register();

// Create child pane classes

WinClassMaker paneClass(WndProcPane, IDC_PANE, hInst);

paneClass.SetSysCursor(IDC_IBEAM);

paneClass.SetDblClicks();

paneClass.Register();

Splitter::RegisterClass(hInst);

// Create top window

ResString caption(hInst, ID_CAPTION);

TopWinMaker topWin(caption, ID_MAIN, hInst);

topWin.Create();

topWin.Show(cmdShow);

В начале мы регистрируем классы. Верхний оконный класс связан с его оконной процедурой WndProcMain, которую мы рассмотрим через мгновение. Два дочерних подокна совместно используют тот же самый класс окна, связанный с WndProcPane. Затем регистрируется наш собственный класс сплиттера (мы скоро увидем его код). В заключение, создается и отображается верхнее окно. Дочерние окна создаются динамически во время инициализации родительского окна.

Приведем оконную процедуру верхнего окна.

LRESULT CALLBACK WndProcMain (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {

 Controller* pCtrl = GetWinLong(hwnd);

 switch (message) {

 case WM_CREATE:

  try {

   pCtrl = new Controller(hwnd, reinterpret_cast(lParam));

   SetWinLong(hwnd, pCtrl);

  } catch (char const* msg) {

   MessageBox (hwnd, msg, "Initialization", MB_ICONEXCLAMATION | MB_OK);

   return -1;

  } catch (...) {

   MessageBox (hwnd, "Unknown Error", "Initialization", MB_ICONEXCLAMATION | MB_OK);

   return -1;

  }

  return 0;

 case WM_SIZE:

  pCtrl->Size(LOWORD(lParam), HIWORD(lParam));

  return 0;

 case MSG_MOVESPLITTER:

  pCtrl->MoveSplitter(wParam);

  return 0;

 case WM_DESTROY:

  SetWinLong(hwnd, 0);

  delete pCtrl;

  return 0;

 }

 return :: DefWindowProc (hwnd, message, wParam, lParam);

}

Имеем обычную оконную процедуру за исключением одного сообщения: MSG_MOVESPLITTER. Это — наше собственное, определяемое пользователем сообщение, которое послано сплиттером его родительскому окну. Но сначала давайте взглянем на контроллер главного окна.

class Controller {

public:

 Controller(HWND hwnd, CREATESTRUCT* pCreat);

 ~Controller();

 void Size(int cx, int cy);

 void MoveSplitter(int x);

private:

 enum { splitWidth = 8 }; // width of splitter

 // User Interface

 HWnd _hwnd;

 //Main controller window

 HWnd _leftWin;

 HWnd _rightWin;

 HWnd _splitter;

 int _splitRatio; // in per cent

 int _cx;

 int _cy;

};

Контроллер содержит дескриптор своего окна, двух дочерних подокон, и окна сплиттера. Он также сохраняет текущий коэффициент разбиения, в процентах.

Конструктор контроллера отвечает за создание дочерних окон.

Controller::Controller (HWND hwnd, CREATESTRUCT * pCreat) : _hwnd (hwnd), _leftWin (0), _rightWin (0), _splitter (0), _splitRatio (50) {

 // Create child windows

 {

  ChildWinMaker leftWinMaker(IDC_PANE, _hwnd, ID_LEFT_WINDOW);

  leftWinMaker.Create();

  _leftWin.Init(leftWinMaker);

  leftWinMaker.Show();

 }

 {

  ChildWinMaker rightWinMaker(IDC_PANE, _hwnd, ID_RIGHT_WINDOW);

  rightWinMaker.Create();

  _rightWin.Init(rightWinMaker);

  rightWinMaker.Show();

 }

 Splitter::MakeWindow(_splitter, _hwnd, ID_SPLITTER);

}

Когда пользователь перемещает полоску расщепителя, родитель получает сообщение MSG_MOVESPLITTER . Параметр wParam содержит новое расстояние полосы расщепителя от левого края родительского окна. В ответ на такое сообщение, родитель должен также изменить размеры дочерних подокон и переместить расщепитель. Он делает это, вызывая метод Size.

void Controller::MoveSplitter (int x) {

 _splitRatio = x * 100 / _cx;

 if (_splitRatio < 0) _splitRatio = 0;

 else if (_splitRatio > 100) _splitRatio = 100;

 Size(_cx, _cy);

}

Вообще же, Size вызывается всякий раз, когда пользователь изменяет размеры основного окна, но тот код который вытолько что видели, мы вызываем также тогда, когда сплиттер перемещается.

void Controller::Size (int cx, int cy) {

 _cx = cx;

 _cy = cy;

 int xSplit = (_cx * _splitRatio) / 100;

 if (xSplit < 0) xSplit = 0;

 if (xSplit + splitWidth >= _cx) xSplit = _cx - splitWidth;

 _splitter.MoveDelayPaint(xSplit, 0, splitWidth, cy);

 _leftWin.Move(0, 0, xSplit, cy);

 _rightWin.Move(xSplit+splitWidth, 0, cx-xSplit-splitWidth, cy);

 _splitter.ForceRepaint ();

}

Обратите внимание на используемый здесь важный прием. Мы перемещаем расщепитель, но задерживаем его перерисовку до изменения его обоих подокон, расположенных слева и справа. Эта методика устраняет некоторое неприятное смазывание изображения.

Выше была представлена реализация клиентского кода. Теперь, я надеюсь, Вам будет интересно увидеть реализацию сплиттера.

Прежде всего нам приятно объединить зависимые функции в пространство имен. Вам видны вызовы Splitter::RegisterClass и Splitter::MakeWindow. Splitter в этих именах — это namespace.

namespace Splitter {

 void RegisterClass (HINSTANCE hInst);

 void MakeWindow (HWnd& hwndSplitter /* out */, HWnd hwndParent, int childId);

};

Ниже приводится реализация этих функций.

LRESULT CALLBACK WndProcSplitter(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);

void Splitter::RegisterClass (HINSTANCE hInst) {

 WinClassMaker splitterClass(WndProcSplitter, "RsSplitterClass", hInst);

 splitterClass.SetSysCursor(IDC_SIZEWE);

 splitterClass.SetBgSysColor(COLOR_3DFACE);

 splitterClass.Register();

}

void Splitter::MakeWindow (HWnd& hwndSplitter, HWnd hwndParent, int childId) {

 ChildWinMaker splitterMaker("RsSplitterClass", hwndParent, childId);

 splitterMaker.Create(); hwndSplitter.Init(splitterMaker);

 splitterMaker.Show();

}

Курсор мыши IDC_SIZEWE мы связываем с классом расщепителя — это стандартная, «направленная с запада на восток», двунаправленная стрелка. Мы также устанавливаем фоновую кисть к COLOR_3DFACE.

Оконная процедура расщепителя имеет дело с созданием/разрушением расщепителя, прорисовкой и перемещением мыши.

LRESULT CALLBACK WndProcSplitter(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {

 SplitController* pCtrl = GetWinLong(hwnd);

 switch (message) {

 case WM_CREATE:

  try {

   pCtrl = new SplitController(hwnd, reinterpret_cast(lParam));

   SetWinLong(hwnd, pCtrl);

  } catch (char const * msg) {

   MessageBox(hwnd, msg, "Initialization", MB_ICONEXCLAMATION | MB_OK);

   return -1;

  } catch (...) {

   MessageBox(hwnd, "Unknown Error", "Initialization", MB_ICONEXCLAMATION | MB_OK);

   return -1;

  }

  return 0;

 case WM_SIZE:

  pCtrl->Size(LOWORD(lParam), HIWORD(lParam));

  return 0;

 case WM_PAINT:

  pCtrl->Paint();

  return 0;

 case WM_LBUTTONDOWN:

  pCtrl->LButtonDown(MAKEPOINTS(lParam));

  return 0;

 case WM_LBUTTONUP:

  pCtrl->LButtonUp(MAKEPOINTS(lParam));

  return 0;

 case WM_MOUSEMOVE:

  if (wParam & MK_LBUTTON) pCtrl->LButtonDrag(MAKEPOINTS(lParam));

  return 0;

 case WM_CAPTURECHANGED:

  pCtrl->CaptureChanged();

  return 0;

 case WM_DESTROY:

  SetWinLong(hwnd, 0);

  delete pCtrl;

  return 0;

 }

 return ::DefWindowProc(hwnd, message, wParam, lParam);

}

Это, в значительной степени, стандартный код. Подробности, как обычно, находятся в методах контроллера. Конструктор очень прост.

SplitController::SplitController(HWND hwnd, CREATESTRUCT * pCreat) : _hwnd (hwnd), _hwndParent (pCreat->hwndParent) {}

Прорисовка более интересна. Мы должны имитировать эффекты 2.5-размерности Windows. Мы делаем это путем тщательного отбора перьев.

class Pens3d {

public:

 Pens3d();

 Pen& Hilight() { return _penHilight; }

 Pen& Light() { return _penLight; }

 Pen& Shadow() { return _penShadow; }

 Pen& DkShadow() { return _penDkShadow; }

private:

 Pen _penHilight;

 Pen _penLight;

 Pen _penShadow;

 Pen _penDkShadow;

};

Pens3d::Pens3d() : _penLight( GetSysColor (COLOR_3DLIGHT)), _penHilight( GetSysColor (COLOR_3DHILIGHT)), _penShadow( GetSysColor (COLOR_3DSHADOW)), _penDkShadow( GetSysColor (COLOR_3DDKSHADOW)) {}

void SplitController::Paint() {

 PaintCanvas canvas(_hwnd);

 {

  PenHolder pen(canvas, _pens.Light());

  canvas.Line(0, 0, 0, _cy - 1);

 }

 {

  PenHolder pen(canvas, _pens.Hilight());

  canvas.Line(1, 0, 1, _cy - 1);

 }

 {

  PenHolder pen(canvas, _pens.Shadow());

  canvas.Line(_cx - 2, 0, _cx - 2, _cy - 1);

 }

 {

  PenHolder pen(canvas, _pens.DkShadow());

  canvas.Line(_cx - 1, 0, _cx - 1, _cy - 1);

 }

}

Более сложной является обработка сообщений от мыши, хотя значительная часть этого кода довольно стандартна. Мы должны обработать перемещение мыши и нажатие кнопки.

void SplitController::LButtonDown(POINTS pt) {

 _hwnd.CaptureMouse();

 // Find x offset of splitter

 // with respect to parent client area POINT

 ptOrg = {0, 0};

 _hwndParent.ClientToScreen(ptOrg);

 int xParent = ptOrg.x;

 ptOrg.x = 0;

 _hwnd.ClientToScreen(ptOrg);

 int xChild = ptOrg.x;

 _dragStart = xChild - xParent + _cx / 2 - pt.x;

 _dragX = _dragStart + pt.x;

 // Draw a divider using XOR mode

 UpdateCanvas canvas(_hwndParent);

 ModeSetter mode(canvas, R2_NOTXORPEN);

 canvas.Line (_dragX, 0, _dragX, _cy - 1);

 }

Когда левая кнопка мыши нажата над клиентской областью расщепителя, мы выполняем следующие задачи. Сначала мы фиксируем мышь. Пользователь может и, возможно будет, перемещают курсор мыши вне полоски расщепителя. Фиксация мыши гарантирует, что все ее сообщения будут теперь направлены к нам, даже в том случае, когда курсор мыши будет блуждать по всему экрану.

Затем мы преобразуем локальные координаты сплиттера в координаты родительского окна. Мы делаем это конвертацией начальных координат родителя к экранным координатам координатам дисплея и преобразованием начальные координаты нашего расщепителя также к экранным координатам дисплея. Разница дает нам начальное положение относительно клиентской области родителя. Мы делаем дополнительные арифметические вычисления, чтобы найти координату x центра расщепителя, потому что это - то, что мы будем перемещать.

Чтобы предоставить пользователю, перемещающему сплиттер, обратную связь, мы рисуем одиночную вертикальную линию, которую будем перемещать поперек клиентской области родительского окна. Обратите внимание на две важных детали. Холст, который мы создаем, связан с родителем — мы используем дескриптор окна родителя. Режим перерисовки использует логическую операцию xor (исключающее или). Это означает, что пикселы, которые мы рисуем, — конвертируются с первоначальными пикселами с использованием xor. Логическая операция xor имеет полезную особенность. Если Вы примените ее дважды, Вы восстановите оригинал. Этот самый старый прием в компьютерной графике — метод простой анимации. Вы рисуете что-то, используя xor режим и стираете простым рисованием его же снова в xor режиме. Итак, рассмотрим код перемещения, представленный ниже.

void SplitController::LButtonDrag (POINTS pt) {

 if (_hwnd.HasCapture()) {

  // Erase previous divider and draw new one

  UpdateCanvas canvas(_hwndParent);

  ModeSetter mode(canvas, R2_NOTXORPEN);

  canvas.Line(_dragX, 0, _dragX, _cy - 1);

  _dragX = _dragStart + pt.x;

  canvas.Line(_dragX, 0, _dragX, _cy - 1);

 }

}

Мы рисуем вертикальную линию в xor режиме, используя предыдущую сохраненную позицию. Так как это рисование происходит во второй раз, мы рисуем эту линию в том же самом месте и, в результате, будут восстановлены первоначальные пикселы из-под этой линии. Затем мы рисуем новую линию в новой позиции, также в xor режиме. Мы запоминаем эту позицию, чтобы в следующий раз, когда вызовется LButtonDrag, мы могли стереть ее также. И так далее, пока пользователь не отпустит кнопку мыши.

void SplitController::LButtonUp (POINTS pt) {

 // Calling ReleaseCapture will send us the WM_CAPTURECHANGED

 _hwnd.ReleaseMouse();

 _hwndParent.SendMessage(MSG_MOVESPLITTER, _dragStart + pt.x);

}

В этой точке мы должны стереть вертикальную строку в последний раз. Однако, мы не делаем это непосредственно — мы используем здесь небольшой прием. Мы прекращаем сбор данных мыши. Как побочный эффект, Windows посылает нам сообщение WM_CAPTURECHANGED. Во время обработки этого сообщения, мы фактически и стираем вертикальную строку.

void SplitController::CaptureChanged () {

 // We are losing capture

 // End drag selection -- for whatever reason

 // Erase previous divider

 UpdateCanvas canvas(_hwndParent);

 ModeSetter mode(canvas, R2_NOTXORPEN);

 canvas.Line(_dragX, 0, _dragX, _cy - 1);

}

Почему мы делаем это таким образом? Потому, что Windows может заставить нас, прекратить обработку данных от мыши прежде, чем пользователь опустит ее кнопку. Это может случиться, например, когда другое приложение внезапно решает распахнуть его окно, в то время как пользователь находится в процессе перемещения. В этом случае наше окно никогда не получило бы сообщение об отжатии кнопки. Если бы мы не были так умны, мы бы не были способны чисто перемещаться. К счастью, перед завершением обработки данных мыши, Windows сумеет послать нам сообщение WM_CAPTURECHANGED, и мы примем его к сведению, чтобы сделать нашу очистку.

Вернемся снова к LBUTTONUP — когда пользователь заканчивает перемещение, мы посылаем родителю наше специальное сообщение MSG_MOVESPLITTER, передавая ему новую позицию центра полосы расщепителя, измеряемой в координатах клиентской области родителя. Вы уже видели клиентский код, реагирующий на это сообщение.

Здесь показано, как можно определить клиентское сообщение.

// Reserved by Reliable Software Library

const UINT MSG_RS_LIBRARY = WM_USER + 0x4000;

// wParam = new position wrt parent's left edge

const UINT MSG_MOVESPLITTER = MSG_RS_LIBRARY + 1;

В заключение представлен фрагмент из очень полезного класса HWnd, который инкапсулирует многие базисных функции API Windows, имеющие дело с окнами. В частности, рассмотрите методы MoveDelayPaint и ForceRepaint, который мы использовали в перерисовке полоски расщепителя.

class HWnd {

public:

 void Update () {

  :: UpdateWindow (_hwnd);

 }

 // Moving

 void Move (int x, int y, int width, int height) {

  :: MoveWindow (_hwnd, x, y, width, height, TRUE);

 }

 void MoveDelayPaint (int x, int y, int width, int height) {

  :: MoveWindow (_hwnd, x, y, width, height, FALSE);

 }

 // Repainting

 void Invalidate () {

  :: InvalidateRect (_hwnd, 0, TRUE);

 }

 void ForceRepaint () {

  Invalidate();

  Update();

 }

 private:

 HWND _hwnd;

};

Как обычно, Вы можете , которое использовалось в этом примере.

Далее: .