SoftCraft
разноликое программирование

Отправная точка
Программирование
Windows API
Автоматы
Нейроинформатика
Парадигмы
Параллелизм
Проектирование
Теория
Техника кодирования
Трансляторы
Прочие вопросы

Разное

Беллетристика
Брюзжалки
Цели и задачи
Об авторе


Как разделить окно на части


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

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

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


Разделительная полоска - полезный элемент, который не входит в состав элементову правления 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<Controller *> (hwnd);

    switch (message)
    {
    case WM_CREATE:
        try
        {
            pCtrl = new Controller 
                (hwnd, reinterpret_cast<CREATESTRUCT *>(lParam));
            SetWinLong<Controller *> (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<Controller *> (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<SplitController *> (hwnd);
    switch (message)
    {
    case WM_CREATE:
        try
        {
            pCtrl = new SplitController 
                      (hwnd, reinterpret_cast<CREATESTRUCT *>(lParam));
            SetWinLong<SplitController *> (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<SplitController *> (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;
};

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

Далее: Следующая обучающая программа рассказывает о растрах.