Статья
Версия для печати
Обсудить на форуме
Элемент управления — Сплиттер (Splitter control).


Автор: Алексей1153

В этой статье представлен класс ALX1153::CSplitter (далее — компонент или просто сплиттер), написанный в среде VC++6 с использованием библиотеки MFC. Код класса протестирован также в VS9. Класс  нашего компонента размещён в пространстве имён ALX1153 и произведён от класса CStatic. Предназначен сплиттер для динамического изменения пользователем размера и положения элементов управления (производных от класса CWnd) на диалоговой форме. Сплиттер, как элемент управления, располагается на любом объекте, класс которого произведён от CWnd (если не вдаваться в крайности, то на объектах типа CDialog, CDialogBar, CFormView  и так далее). Также на нашем сплиттере можно отображать полосу прогресса. Код класса открыт, можете делать с ним что угодно.
Исходный код компонента представлен в 2 файлах (их можно взять из файлов проекта, который находится по ссылке в конце статьи):

Splitter1153.h заголовочный файл класса компонента.
Splitter1153.cpp файл реализации класса компонента.

* * *

Тестовый проект для этой статьи, как уже было сказано, можно найти по ссылке в конце статьи. Но представим, что проект ещё не создан. Здесь будет описано, как производится вставка компонента в проект и работа с ним.
Создаём тестовый проект SplitterTest (MFC, Dialog-based). Копируем в папку с проектом файлы класса компонента, описанные выше. Добавляем файлы также в дерево файлов проекта. После этого рекомендуется удалить из папки проекта файл «имя_проекта.clw» (можно даже не закрывая студию). Затем в студии нажать Ctrl+W для того, чтобы обновилось дерево классов.
Описываемый класс является производным от MFC-класса CStatic. Поэтому, чтобы поместить компонент на диалоговое окно в режиме редактирования ресурсов, надо положить на диалог контрол CStatic из стандартной  палитры конторолов. Затем надо добавляем для статика связанный с ним член-переменную: удерживая Ctrl, дважды щелкаем по статику, пишем имя переменной m_....., в окне Category -> выбираем Control,  в окне Variable Type -> выбираем временно класс CStatic (так как визард не умеет заглядывать в пространство имён). Также не забываем добавить в заголовочный файл диалога перед описание класса диалога строчку:

Код:
#include "Splitter1153.h"

Тут надо отметить следующий момент. Сплиттер может быть горизонтальный или вертикальный. Когда CStatic кладётся на форму, это никак не предопределено, это будет задано в свойствах объекта в программе. Тем не менее, можно для наглядности придать статику форму, близкую к желаемой. Поскольку границу у статика (когда в нем нет текста) в редакторе ресурсов не видно,  то для того, чтоб сделать статик заметным  можно вписать, например, «= = = » (чередование «=» и пробелов) по всей длине статика. В вертикальном статике, за счёт наличия пробелов, «=» расположатся на всех строчках и равномерно распределятся по высоте.
У сплиттера имеется два размера — «основной» и «не основной». Для горизонтального основной — это высота. Для вертикального — ширина. Контрол следит только за своим основным размером, а второй размер остаётся такой, какой был установлен в редакторе ресурсом. Конечно, его можно изменить в процессе работы программы методом  CWnd::MoveWindow(). Величина основной размера может задаваться через метод контрола.

Итак, для примера создадим два сплиттера — горизонтальный и вертикальный. Зададим идентификаторы IDC_SP_H (для горизонтального) и IDC_SP_V (для вертикального). Добавляем для них связанные переменные класса CStatic с именами m_IDC_SP_H и m_IDC_SP_V соответственно (как описано выше — удерживая Ctrl, дважды щелкаем). Поставьте в свойствах статиков следующие галочку Notify. Если не поставить галочку, то контрол не будет реагировать на щелчки мышью. Не забываем добавить в заголовочный файл диалога перед описанием класса диалога строчку:

Код:
#include "Splitter1153.h"

Также вручную исправим CStatic на ALX1153::CSplitter.

Код:
class CSplitterTestDlg : public CDialog
{
...
//{{AFX_DATA(CSplitterTestDlg)
...
ALX1153::CSplitter m_IDC_SP_H;
ALX1153::CSplitter m_IDC_SP_V;
//}}AFX_DATA
...
};

Компилируем проект (нет ли ошибок?) и сохраняем.

* * *

Нам понадобятся ещё некоторые элементы управления, которые будут участвовать в демонстрации. Набросаем на форму несколько элементов, скажем Edit, которые будут, собственно, «двигаться» сплиттерами. Дадим им ID == IDC_EDIT1, IDC_EDIT2, IDC_EDIT3, IDC_EDIT4.
Теперь вся наша форма выглядит примерно так:

                                                 
IDC_EDIT1


                                                 
IDC_EDIT2


                                                 
IDC_EDIT3

                                                 
IDC_EDIT4

То есть вертикальный сплиттер делит форму на 2 части. Справа — элементы IDC_EDIT3 и IDC_EDIT4, слева — горизонтальный сплиттер, над которым — IDC_EDIT1, а под — IDC_EDIT2.

Теперь надо создать объект каждого сплиттера. Если бы главное окно было дитём от CView, то создание нужно было бы делать в методе OnInitialUpdate(). В диалоге создание производим в методе OnInitDialog() (обработчик сообщения WM_INITDIALOG).
Создаётся сплиттер методом Create(...):

Код:
BOOL ALX1153::CSplitter::Create(
CWnd* pParent,
bool bHorizontal,
bool bAutoArrowEnable
)

В методе OnInitDialog(...) пишем:

Код:
//создание горизонтального сплиттера
m_IDC_SP_H.Create(this,true,true);

//создание вертикального сплиттера
m_IDC_SP_V.Create(this,false,true);

Пол дела уже сделано. Можете скомпилировать и запустить проект. Убедитесь, что уже можно передвигать сплиттеры мышкой. Обратите внимание, что «не основной» размер остался таким, как вы указали в редакторе ресурсов. Как мы и указали в Create(...), автоматически появляется двухсторонняя стрелка-курсор.
Если попытаться задвинуть сплиттер за край родительского окна, он будет против этого и остановится у самого края. Однако, если начать двигать край самого родительского окна, то есть риск оставить сплиттер за краем... Как с этим бороться, будет рассказано ниже (метод OnWindowPosChanged(...) родительского окна).

* * *

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

IDmess_OnChangePos Пользователь перемещает сплиттер

Для реализации обработки сообщений добавляем в главный диалог виртуальную функцию OnNotify(...).
В начале файла Splitter1153.h приведён пример, как обрабатывать сообщения от нашего контрола. Код вставляется в OnNotify(...) родительского окна:

Код:
//обработка сообщений от сплиттера CSplitter
{{{
//ЗДЕСЬ УКАЗЫВАЕТСЯ ИДЕНТИФИКАТОР СПЛИТТЕРА
ALX1153_BEGINMAP_Splitter(IDC_xxxxxxx)
ALX1153_SWITCH_Splitter
{
case pSP->IDmess_OnChangePos:
{
//...
}
break;

//case...
}
ALX1153_ENDMAP_Splitter_and_return1_if_processed
}}}

Пояснения:
  • IDC_xxxxxxx — идентификатор контрола-сплиттера в ресурсах.
  • Добавьте свой код блоках case обработчиков.
  • Если сообщение не было предназначено для контрола, идентификатор которого указан в enum{...}, то станет выполнятся код функции OnNotify(), расположенный далее «}}}». Если же сообщение обработано, то произойдёт выход из OnNotify() со значением 1 («сообщение обработано»).

В блоках case обработчиков доступны следующие переменные :

ALX1153::CSplitter *pSP; Указатель на объект контрола
const NMHDR* pNMHDR; указатель на структуру NMHDR

* * *

С обработкой сообщений вроде разобрались.
Итак, как бороться с уползанием сплиттера за край родительского окна? Добавим в тестовый диалог диалог обработчик сообщения WM_WINDOWPOSCHANGED (посылается окну, когда меняется его размер), и напишем две строки — корректировку положения сплиттеров.

Код:
void CSplitterTestDlg::OnWindowPosChanged(WINDOWPOS FAR* lpwndpos)
{
CDialog::OnWindowPosChanged(lpwndpos);

//корректировака только сплиттеров
m_IDC_SP_H.RefreshCurrentRectFromParent();
m_IDC_SP_V.RefreshCurrentRectFromParent();
}

Запустите программу. Обратите внимание, как теперь ведут себя сплиттеры, если при изменении размеров диалога они оказываются возле самого края диалога — сплиттеры сдвигаются. Однако, они помнят последнюю позицию, заданную им мышью (или методом ALX1153::CSplitter::MoveWindow_SetTheMinimumMainCoordinate(...). Обратите внимание на этот момент — если вы хотите программно, а не мышью, переместить сплиттер, то кроме MoveWindow(), новую минимальную координату «основного размера» надо указать в упомянутом методе MoveWindow_SetTheMinimumMainCoordinate.). Если начать увеличивать размер диалога, сплиттеры будут стремиться вернуться к прежнему положению.
Теперь добавим в диалог метод RefreshControlsPositions(), в котором происходит перемещение всех элементов управления в соответствии с текущим положением сплиттеров. Этот метод нужно вызвать из обработчиков сообщения IDmess_OnChangePos всех сплиттеров, а также в методе OnWindowPosChanged(...) (вообще — после любого перемещения сплиттеров):

Код:
BOOL CSplitterTestDlg::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
//обработка сообщений от сплиттера
{{{
...
case pSP->IDmess_OnChangePos:
{
RefreshControlsPositions();
}
break;
...
}}}

//обработка сообщений от сплиттера
{{{
...
case pSP->IDmess_OnChangePos:
{
RefreshControlsPositions();
}
break;
...
}}}
...
}

void CSplitterTestDlg::OnWindowPosChanged(WINDOWPOS FAR* lpwndpos)
{
CDialog::OnWindowPosChanged(lpwndpos);

// //корректировака только сплиттеров
// m_IDC_SP_H.RefreshCurrentRectFromParent();
// m_IDC_SP_V.RefreshCurrentRectFromParent();

//передвижение всех контролов диалога
RefreshControlsPositions();
}

В методе RefreshControlsPositions все вычисления производятся в абсолютных координатах, а перемещение каждого подвижного элемента управления (только кнопки в нашем примере не перемещаются) производится макросом def_MW, который переводит прямоугольник в координаты клиентской области перед вызовом MoveWindow(). Переменная nTopBorder задаёт верхний отступ. Происходит следующее:
  • Определяются максимальные и минимальные координаты клиентской области;
  • В соответствии с режимом перемещения окошек (кнопка «Режим размещения» на диалоге) задаём положения сплиттеров и их отступы.
  • Добываются прямоугольники сплиттеров — rH и rV (нужны будут для дальнейших расчётов).
  • В соответствии с режимом перемещения окошек размещаем все окошки CEdit.
Все подробности описаны в комментариях к коду в проекте.

* * *

На диалоге расположены кнопки и переключатели для демонстрации некоторых возможностей сплиттеров:
  • Показ полосы прогресса. В проекте изменение значения полосы прогресса сделано по сообщению WM_MOUSEMOVE. При перемещении указателя мыши по диалогу полоски прогресса уменьшаются (удобно смотреть при «режиме размещения», когда справа есть чистая область диалога).
  • Режим размещения — переключение между двумя (из, конечно же, множества возможных) вариантами размещения окошек.
  • Невидимые границы — режим «невидимых» сплиттеров.
  • Передвижение сплиттеров программно — демонстрация работы метода MoveWindow_SetTheMinimumMainCoordinate(...)).

* * *

Ну вот, в принципе, и всё. Кстати —  в заголовочном файле содержится много комментариев о назначении методов и переменных, а в файле реализации прокомментировано внутреннее устройство методов.

Как и всегда надеюсь, что мой компонент окажется полезен. :)

Весь тестовый проект (вместе с файлами класса, они лежат в папке проекта) можно найти здесь.
Версия для печати
Обсудить на форуме