"Программирование на Visual C++. Архив рассылки" - читать интересную книгу автора (Jenter Алекс)

Программирование на Visual C++ Выпуск №17 от 29 сентября 2000 г.

Всем привет!

До меня дошли сведения, что предыдущий, 16-тый, выпуск дошел почему-то не до всех подписчиков. То ли из-за глюков на ГорКоте, то ли из-за гиперактивности магнитных бурь … ;)

Повторно высылать я его, пожалуй, не стану, лучше дам вам на него прямую ссылку. Тем, кто его еще не видел, рекомендую прочитать,так как данный выпуск связан с предыдущим.

MFC

Итак, наконец-то мы добрались до темы создания окон свойств, которые в MFC реализуются с помощью, как вы уже догадались, класса CPropertySheet.

Работа с окнами свойств : использование класса CPropertySheet

Как известно, практически во всех более-менее серъезных программах есть диалоговые окна настройки параметров, или опций, приложения. Такие окна получили название окон свойств. Чтобы увидеть одно из таких окон, достаточно выбрать Tools|Options в Visual C++ IDE.

Задача создания окон свойств стоит практически перед каждым разработчиком, именно поэтому в MFC решение этой проблемы в некоторой степени автоматизировано с помощью класса CPropertySheet. В результате его применения вы получаете готовое диалоговое окно с набором закладок и некоторым количеством стандартных кнопок – OK, Cancel, и т.д. Закладки здесь – это объекты типа CPropertyPage. Этот класс, кажется, уже фигурировал в одном из выпусков. Никогда не путайте CPropertySheet и CPropertyPage: помните, что первый (CPropertySheet) СОДЕРЖИТ вторые (CPropertyPage) так же, как книга содержит страницы.

Так, с этим разобрались, идем дальше. Как пользоваться классом CPropertySheet? Очень несложно, вы в этом сами сейчас убедитесь.

Для каждой закладки нужно создать диалоговый ресурс (не обязательно со стилем child), куда вы помещаете все содержимое соответствующей страницы (также, как и при работе с CTabCtrl). Например, IDD_PROPPAGE1 и IDD_PROPPAGE2ROPPAGE2. Можно сразу заполнить поле Caption в диалогах, чтобы потом заголовки закладок сформировались автоматически.

В проект добавляется класс-наследник от CPropertySheet, пускай он называется CMyPropSheet.

Для того, чтобы можно было работать с контролами на закладках, добавляется отдельный класс для каждой страницы-закладки (наследованный от CPropertyPage). Например, для двух закладок это будут классы CPropPage1 и CPropPage2 (эти классы добавьте дабл-кликнув на поверхности соответствующего диалога и выбрав "Create a new class", затем в поле "Base class" выберите CPropertyPage в качестве класса-родителя). В эти классы нужно поместить члены, связанные с контролами, расположенными на странице. Например, если у нас на первой странице (IDD_PROPPAGE1) есть Edit Box, добавляем в класс CPropPage1 переменную m_strEdit класса CString, доступ – public. Пусть на второй странице у нас Check Box, значит в класс CPropPage2 записываем член m_isChecked типа BOOL, и т.д. Использование типа доступа public к этим полям в данном случае оправданно, т.к. избавляет в дальнейшем от многих хлопот. И не забывайте, эти члены класса должны быть связаны с соответствующими контролами на закладке.

Теперь в файл mypropsheet.h (где объявлен класс CMyPropSheet) пишем:

#include "proppage1.h" // делаем классы страниц видимыми

#include "proppage2.h"

class CMyPropSheet: public CPropertySheet {

 …

protected:

 CPropPage1 page1; // первая страница

 CPropPage2 page2; // вторая страница

 …

}

Чтобы добавить страницы к окну свойств, необходимо в каждый из конструкторов CMyPropSheet вставить по две следующие строчки:

AddPage(amp;page1);

AddPage(amp;page2);

Таким образом, мы сделали страницы-закладки частью нашего окна свойств. Настал момент теперь решить, где вы собираетесь хранить настройки вашего приложения. Я это обычно делаю в классе главного окна, по причине легкости доступа, но ничто не мешает вам хранить их там, где удобнее, причем не обязательно все вместе. Неплохой вариант – хранить их в классе приложения, даже с некоторой точки зрения он более логичный.

Но предположим, вы решили хранить их в классе главного окна, а чтобы они не перемешивались с другими полями класса, объединить их в структуру:

class CMainFrame: public CFrameWnd {

 …

public:

 struct Options {

  CString str;

  BOOL val;

 } options;

 …

}

В конструкторе CMainFrame поля структуры options установите в начальные значения. Их можно задать жестко, но обычно сохраненные ранее значения загружаются из файла или реестра , иначе вы быстро доведете пользователя вашей программы до белого каления, заставляя его менять параметры после каждого запуска.

Теперь вставьте обработчик события, возникновение которого должно приводить к выводу на экран вашего окна свойств (например, выбор пункта меню "Сервис|Параметры…"). В обработчике вы устанавливаете параметры, после чего выводите окно свойств. Если пользователь нажал "OK", то после закрытия окна свойств нужно обновить структуру options:

#include "mypropsheet.h"

void CMainFrame::OnToolsOptions() {

 CMyPropSheet ps("Параметры приложения", this, 0);

 ps.page1.m_strEdit = options.str;   // настраиваем закладки

 ps.page2.m_isChecked = options.val; // соответственно текущим параметрам

 if (ps.DoModal() == IDOK) // если пользователь нажал OK

 {

  options.str = ps.page1.m_strEdit; // сохраняем параметры

  options.val = ps.page2.m_isChecked;

 }

}

Вот и все, что касается элементарного использования класса CPropertySheet для создания окна свойств. Как видите, работать с ним довольно просто. В одном из следующих выпусков я расскажу вам о задействовании кнопки "Применить" ("Apply"), о нетривиальном использовании этого класса для создания мастеров (wizards), а также о расширенном классе CPropertySheetEx.

ВОПРОС-ОТВЕТ

Q. Возникла вот такая задачка. Имеется некоторое разбиение SDI на несколько view при помощи сплиттеров (A). Как его изменить не убивая окна (на B или C)?

  +--+----+     +--+----+    +--+----+

  |  |    |     |  |    |    +  +    +

  +--+----+     +--+    |    +  +----+

  |       |     |  |    |    +  +    +

A +-------+   B +--+----+  C +--+----+

Nikita Zeemin

A. Для создания окна сплиттера в MFC служит класс CSplitterWnd. Этот класс предоставляет функции для создания вида (CreateView) и удаления вида DeleteView) в заданной панели, но не предоставляет функции, которая позволила бы перенести вид из одной панели в другую. Чтобы проделать это вручную, нужно понимать, каким образом связаны объект класса CSplitterWnd и объекты дочерних видов CView.

CSplitterWnd может иметь не более 16 панелей по горизонтали и столько же по вертикали. Таким образом, он может сожержать не более 256 панелей. Каждой панели соответствует уникальный идентификатор, который и назначается тому виду, который в этой панели находится. Отображение координат панели на её идентификатор выполняет функция int CSplitterWnd::IdFromRowCol(int row, int col);

На самом деле после целой серии ASSERT'ов она просто возвращает значение

AFX_IDW_PANE_FIRST + row * 16 + col

где AFX_IDW_PANE_FIRST — константа, объявленная в MFC.

Это подсказывает простой способ перемещения вида из одной панели в другую: нужно всего лишь подменить его идентификатор, после чего вызвать CSplitterWnd::RecalcLayout для обновления содержимого сплиттера. Если изначально вид не являлся дочерним окном сплиттера и требуется поместить его в одну из панелей, то необходимо также поменять ему родителя с помощью функции CWnd::SetParent. Таким образом функция, вставляющая вид в заданную панель, может выглядеть примерно так:

class CMySplitter : public CSplitterWnd {

 …

 void InsertView(int nRow, int nCol, CWnd *pView) {

  pView-gt;SetParent(this);

  pView-gt;SetDlgCtrlID(IdFromRowCol(nRow, nCol));

 }

 …

}

Аналогичным образом вид переносится "в юрисдикцию" главного окна приложения, порождённого от CFrameWnd:

class CMyFrame : public CFrameWnd {

 …

 void InsertView(CWnd *pView) {

  pView-gt;SetParent(this);

  pView-gt;SetDlgCtrlID(AFX_IDW_PANE_FIRST);

 }

 …

}

Имея в руках эти две функции, можно без труда решить поставленную задачу, располагая виды как на рис. A, B, C или любым другим способом. Ещё раз замечу, что после всех перемещений необходимо вызывать CSplitterWnd::RecalcLayout и CFrameWnd::RecalcLayout.

Alexander Shargin
ОБРАТНАЯ СВЯЗЬ

Alexander Shargin, воистину герой сегодняшнего выпуска, в письме с ответом на предыдущий вопрос также недоумевал по поводу вопроса прошлого выпуска, где требовалось минимизировать CPropertySheet:

…Честно говоря, я не понимаю, о чём идёт речь. Я буду очень вам признателен, если вы объясните мне, в чём проблема. Дело в том, что я создал визардом приложение на базе диалога, а затем просто заменил диалог на объект класса CMySheet, порождённого от CPropertySheet. После чего добавил пару вкладок (типа CPropertyPage) и вызов ModifyStyle(0, WS_MINIMIZEBOX) в обработчике OnCreate. В результате этих несложных операций получилось приложение, главное окно которого без проблем сворачивается на панель задач.

Я посмотрел приаттаченный им проект и убедился, что Александр совершенно прав. После чего я сам проделал то же самое с новым проектом, и получил такой же результат. Итак, вся загвоздка была в том, что ModifyStyle нужно было вызывать не из OnInitDialog, а из OnCreate!

После этого я задумался, откуда же у класса CPropertySheet вообще есть метод OnInitDialog, ведь сам класс является прямым наследником CWnd. Оказалось, что этот метод, наряду с DoModal, был добавлен туда искусственно, чтобы обращение с классом напоминало обращение с CDialog. Не знаю, почему бы Microsoft просто не сделать CPropertySheet наследником CDialog, но наверное у них были свои причины (хотя здесь можно и посомневаться ;)

Я переслал письмо Александра человеку, задавшему вопрос, и получил от него положительный ответ – у него тоже все заработало.

Вот, оказывается, как просто открывался ларчик! Не надо было перехватывать WM_NCLBUTTONDOWN, не нужно было делать callback функцию… (решение автора вопроса)…

И еще – насчет минимизации в левый нижний угол – видимо это был частный случай поведения, вызванный моими манипуляциями со стилями ;-)

Напоследок хочу процитировать Win32 Qamp;A из MSDN, чтобы абсолютно точно уяснить для всех

Как Windows определяет, нужно ли выводить кнопку приложения на панель задач

Правила эти довольно просты, хотя и не очень хорошо документированы. Когда вы создаете окно, Windows проверяет его расширенный стиль. Если установлен стиль WS_EX_APPWINDOW (определенный как 0x00040000), на панель задач выводится кнопка окна. Если же установлен стиль WS_EX_TOOLWINDOW (0x00000080), то кнопка не выводится. Не следует создавать окна, где установлены оба эти стиля.

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

И последнее замечание: прежде чем тестировать что-либо из вышеописанного, панель задач проверяет, установлен ли стандартный стиль видимости WS_VISIBLE. Если нет, то окно спрятано, и показывать кнопку нет никакого смысла. Стили WS_EX_APPWINDOW, WS_EX_TOOLWINDOW и информация о принадлежности окна проверяются ТОЛЬКО при установленном  WS_VISIBLE.

Jeffrey Richter
В ПОИСКАХ ИСТИНЫ

Q. У меня программа с использованием MFC и Doc/View. Я вставил RichEditCtrl во вью. (2-ой версии). Установил шрифт с помощью сообщения SetCharFormat. Внимание, вопрос: почему если я ввожу текст с клавиатуры и использую ReplaceText функцию (не сообщение!) фонты различные? Вроде это сообщение не менялось у второй версии. Заранее спасибо за ответ.

Игорь

Это все на сегодня. Пока!

© Алекс Jenter mailto:[email protected] Красноярск, 2000.