Автор:
Алексей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.
Теперь вся наша форма выглядит примерно так:
То есть вертикальный сплиттер делит форму на 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(...)).
* * *
Ну вот, в принципе, и всё. Кстати — в заголовочном файле содержится много комментариев о назначении методов и переменных, а в файле реализации прокомментировано внутреннее устройство методов.
Как и всегда надеюсь, что мой компонент окажется полезен. :)
Весь тестовый проект (вместе с файлами класса, они лежат в папке проекта) можно найти
здесь.