| www.it-effect.com.ua |
|
| На центральную | Что нового | Что интересного | Структура сайта... |
| ПРО ЭТО... | Что здесь нового | Что здесь интересного |
Такая хитрая вещь придумалась: объект, который также есть список (т.е. содержит указатель на следующий объект того же класса), и который читается из реестра Windows из ключей вида "NAMExxx", где xxx - это число от 0 до N. Так вот, этот объект список должен прочитаться независимо от того, сколько элементов "NAMExxx" есть в соответствующем разделе реестра и с какого номера они начинаются.
Первый вариант решения: создаем объект #0 и читаем его его из реестра. После прочтения себя самого он ищет следующий элемент от 0+1, т.е. от 1, и добавляет его себе. А тот, в свою очередь, следующий, и так далее.
Ошибка: если в реестре нет элемента #0, то список прочитан не будет! Решение: объект сам должен искать следующий существующий ключ реестра!
CString при передаче параметров аргументу char* ругается. Надо писать const char*.
А если уж на то пошло, то неплохо бы вообще писать LPSTR и LPCSTR соответвенно. А еще лучше: LPTSTR и LPCTSTR! В чем разница? Те, что с буквой "T" - это потенциальные строки UNICODE. Не знаю как вы, а я уже привык все строковые константы обрамлять макросом _T()...
И вообще, повсеместное использование аргуметов char* сильно сбивает с толку: непонятно, то ли это входящий аргумент функции, то ли это буфер для результата. Особенно если саму функцию писал не ты. Приведение "в лоб" (LPSTR) (LPCSTR) strSomeParam - попытка передачи CString как параметра char* - может привести и скорее всего приведет к ошибке, если это как раз именно буфер для результата. Вот такие вот проблемы интеграции "гетеропрограммистского" исходного кода... :)
Для программного выбора радиокнопки используйте функцию
BOOL CheckRadioButton(
HWND hDlg, // handle to dialog box
int nIDFirstButton, // identifier of first radio button in group
int nIDLastButton, // identifier of last radio button in group
int nIDCheckButton // identifier of radio button to select
);
Вообще-то, MFC, имеет встроенный механизм обмена данным с группами радиокнопок (кнопок выбора - Radio Buttons). Для того, чтобы им воспользоваться, необходимо для первой кнопки в группе добавить свойство "Group" в диалоге свойств этого элемента управления в редакторе ресурсов. После чего этот элемент появится в списке доступных для обмена данными. Нажимаем Ctrl и делаем двойной щелчок на этой кнопочке. В открывшемся окне "Add Member Variable" задаем имя переменной. Больше задать ничего и не получится, так как там можно задать только имя и только переменную типа int. После этого выбор в группе Radio Buttons определяется значением этой переменной - порядковым номером кнопки в группе, считая с 0.
Однако, при использовании этого механизма в чуть более сложном диалоге я столкнулся с проблемой. Точнее, с "особенностью работы". Дело в том, что я реализовал доступность некоторых кнопок выбора в зависимости от еще одной группы Radio Buttons. При этом появилась возможность возникновения "потерянного выбора" - это когда кнопка в группе выбрана, но недоступна. Пользователь не может изменить состояние такой кнопки - она остается выбранной, даже если выбрана другая, доступная кнопка. И при передаче данных от элементов управления в переменные (DoDataExchange) функция DDX_Radio(...) выбрасывает исключение. Очевидно, что необходимо позаботиться о "правильном выборе" самостоятельно, например, с помощью принудительного программного выбора одной из доступных кнопок посредством функции CheckRadioButton(...).
Если вам необходимо описать набор каких-то значений, используемый для представления нескольких состояний чего-либо и передачи этого в диалог etc., используйте enum в маленьком заголовочном файле типа:
#if !defined(AFD_YOURVALNAME_H__INCLUDED_)
#define AFD_YOURVALNAME_H__INCLUDED_
enum eYourValName
{
VAL_UNDEFINE = 0,
VAL_FIRST = 1,
VAL_SECOND,
...
};
#endif // !defined(AFD_YOURVALNAME_H__INCLUDED_)
что позволяет включать его по необходимости куда угодно, не беспокоясь об ошибке переопределения, и использовать единые символические имена на протяжении всего проекта.
Кстати, AFD_... - это просто от названия моей компании. Так MS Visual C++ использует на этом месте AFX_... Позволяет неявно так несколько подчеркнуть авторство.
В процессе разработки программного комплекса, включающего несколько приложений, я столкнулся с проблемой "не работает и все тут!" Правда решение проблемы я нашел довольно быстро: типы объектов представлялись с помощью enum и при добавлении нового типа в середину последоватальности возникло расхожение в значениях второй половины. Вам понятно, что я имею в виду? Решение: либо явно указывать значения перечисления:
enum eYourValName
{
VAL_UNDEFINE = 0,
VAL_FIRST = 1,
VAL_SECOND = 2,
VAL_THIRD = 3,
...
};
, либо добавлять новые элементы только в конец последовательности:
enum eYourValName
{
VAL_UNDEFINE = 0,
VAL_FIRST,
VAL_SECOND,
...,
VAL_NEW
};
Теоретически, можно еще внимательно следить за перекомпиляцией всех приложений комплекса, однако это редко реально возможно: подумайте о пользователях вашей программы, работающих с более старыми версиями! Конечно же, если у вашей программы вообще есть пользователи, кроме вас самих... :)
Для получения немодального диалога можно добавить указатель на необходимый класс в какое-нибудь место, например класс родительского окна, и инициализировать его один раз. Это можно сделать либо в конструкторе родителя, либо в функции, предназначенной для отображения данного диалога. Для отображения вызывается функция Create(), которую для удобства работы необходимо перегрузить. Для повторных вызовов Create вызывать не надо, необходимо использовать ShowWindow(SW_SHOW). Функция вызова этого диалога может быть такой:
void CYourOwner::OnProjectSettings()
{
if(m_pDlg == NULL)
{
m_pDlg = new CProjectDlg;
m_pDlg->Create();
}else
m_pDlg->ShowWindow(SW_SHOW);
}
При этом на все время существования родителя открывается один диалог. Для каждого экземпляра родителя (или хозяина). Да, и не забудте в деструкторе хозяина удалить указатель на класс диалога.
... delete m_pDlg; ...
Но вообще-то пожалуй удобнее в конструкторе хозяина инициализировать указатель не NULL, а создавать диалог. Это позволит избавиться от повсеместной проверки наличия отличия указателя от NULL во всех возможных местах использования.
Несколько запутавшись в создании нового окна для вывода параметров модели во время работы программы, я создал диалог, назначил тип рамки Resizing и стиль Overlapped, добавил элемент списка и заставил его отслеживать размеры диалога - список на 8 чего-бы-там-ни-было уже клиентской области диалога. Т.е.:
void COutputDlg::OnSize(UINT nType, int cx, int cy)
{
CDialog::OnSize(nType, cx, cy);
CRect rect;
GetClientRect(&rect);
rect.DeflateRect(8, 8);
m_cListOut.MoveWindow(rect);
}
Здесь я натолкнулся на одну, пока невыясненную мною, ошибку (точнее "глюк"): если оставить у диалога стиль Popup - при первоначальном запуске попытка изменить размеры окна списка выбрасывает исключение, которое тем не менее можно проигнорировать, и в дальнейшем все работает без каких-либо эксцессов.
Я понял!!! Здесь попытка работать с окнами, а окон то и нет! Представители классов-оболочек есть, а окон - нет. И это только при первом сообщении. А далее все путем. Т.е. код надо изменить так:
void COutputDlg::OnSize(UINT nType, int cx, int cy)
{
CDialog::OnSize(nType, cx, cy);
if(m_bInit)
{
CRect rect;
GetClientRect(&rect);
rect.DeflateRect(8, 8);
m_cListOut.MoveWindow(rect);
}
}
А переменную m_bInit устанавливать в TRUE в OnInitDialog (обработчик WM_INITDIALOG). И не забудьте
в конструкторе класса-оболочки диалога сбросить ее в FALSE.
В элементе управления MFC (ну и Win32 API соответственно) CListCtrl почему-то нет метода для удаления всех столбов (а может я не нашел?). Есть только метод для удаления одного столбца:
BOOL DeleteColumn( int nCol );
Получая исключения в случаях неудачи удаления, я довольно быстро сообразил, что для удаления всех столбхов необходимо, пока это возможно, удалять столбец номер 0:
while(m_cListOut.DeleteColumn(0));
Странно, что это не реализовано разработчиками, но, наверное, не так часто возникает необходимость удалить все столбцы сразу.
Если предполагается изменение пользователем параметров сколь бы то ни было сложного объекта лучше сразу предположите для этого специальный метод в классе самого объекта. При малейшем расширении программы вы "неожиданно" сталкиваетесь с потребностью вызывать диалог свойств объекта в одной-двух тысячах мест...
Вообще-то это несколько противоречит принципу разбиения на "алгоритм" и "интерфейс". Очень часто бывает удобно, чтобы код "алгоритма" не использовал функции, специфичные для "интерфейса". Так, например, "алгоритм" может быть реализован на "чистом C++" и не использовать ни одного API. В то время как "интерфейс" без взаимодействия с операционной системой реализовать вообще не представляется возможным. Опять таки, "интерфейс" может быть не только с пользователем, а, например, с файлом на диске или с БД.
Я считаю, что точку взаимодействия "алгоритм"-"интерфейс" ("объект"-"диалог свойств объекта") можно вынести в одно место тремя способами:
bool ChangeProps(CMyObject& obj)
{
CMyDlg dlg;
dlg.m_strProp_1 = obj.m_strProp_1;
...
if (dlg.DoModal() == ID_OK)
{
obj.m_strProp_1 dlg.m_strProp_1;
...
return true;
}
else
return false;
}
bool CMyObject::ChangeProps();
CMyObject obj;
...
CMyObjectPropsDlg dlg(obj);
if (dlg.DoModal() == IDOK)
{
...
}
Надо над этим еще подумать, все же интерфейсов весьма сложных к весьма непростым объектам написал я не слишком мало...
Древовидный (иерархический - как кому нравиться) список - удивительно удобное средство для отображения какой-либо структуры! В его реализации в Win32 есть очень полезная вещь: каждый элемент списка имеет в своем составе поле типа DWORD. Естественно, сразу же приходит на ум прописать туда указатель на объект, соответствующий элементу списка на экране.
Кстати в Delphi это работает не менее эффективно (и эффектно)...
Для получения такого элемента управления, как мигающая лампочка-пимпочка, можно воспользоваться тем, что елемент Static позволяет подставлять вместо текста (а может и не только вместо) иконку или битмапчик. Просто меняя иконки функцией SetIcon того же CStatic мы получим возможность динамического отображения режима работы чего-бы-то-ни-было, например, светофора.
А вот для хранения списка иконок или изображений я не придумал ничего лучше, чем организовать масив для ряда хэндлов иконок/битмапов, инициализировать этот массив перед использованием изображений. А надо ли его деинициализовать? Хм...
Все конечно же знают, как показать контекстное меню. Откликнуться на WM_CONTEXTMENU и нарисовать соответствующее popup-меню... Ну, насчет WM_CONTEXTMENU - это как-нибудь потом, а код вызова popup я здесь приведу:
...
CMenu menu;
menu.LoadMenu(IDR_CONTEXTMENU);
CMenu* submenu = menu.GetSubMenu(0);
ASSERT(submenu);
submenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON,
point.x, point.y, this);
...
Разные предложения по оптимизации вполне рациональны и применимы, но так проще...
Так как я сам иногда заглядываю к себе посмотреть, как я делал что-то ранее, то я использовал этот код для показа контекстного меню. Выяснилась интересная особенность: при вызове контекстного меню с помощью мыши все работает правильно, а при вызове посредством клавиатура - нет. В этом случае вместе с сообщением WM_CONTEXTMENU передаются координаты (-1, -1). Т.е. меню отображается "там, где-то там" в левом верхнем углу экрана.
Чтобы обойти это я видоизменил код таким образом:
...
CMenu menu;
menu.LoadMenu(IDR_CONTEXTMENU);
CMenu* submenu = menu.GetSubMenu(0);
ASSERT(submenu);
CPoint subpoint(8, 8);
ClientToScreen(&subpoint);
if (point.x < subpoint.x || point.y < subpoint.y)
point = subpoint;
submenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON,
point.x, point.y, pWnd);
...
Точка (8, 8) мне просто под руку попалась. Как и всякий программист, я стремлюсь использовать числа либо степени двойки, либо кратные им. Так как на поверку (16, 16) оказалось много и не слишком эстетично, то было взято вдвое меньшее значение. Логично и много думать не надо. Хотя, как мне кажется, точка (4, 4) получше будет... :)
ASSERT(g_pMainLayout->AddWindow(GetSafeHwnd()));и
BOOL bTest = g_pMainLayout->AddWindow(GetSafeHwnd()); ASSERT(bTest);
В первом случае код несомненно красивее. Но вот работает такой код только в режиме отладки. А в Relese-версии новые окна просто не добавляются! Хм...
Оказывается в таком случае можно написать так - точнее, именно так и нужно писать:
VERIFY(g_pMainLayout->AddWindow(GetSafeHwnd()));
На эту тему есть статья в RSDN.
По-поводу разбиения проекта на некоторое количество файлов, включая *.h, *.cpp, *.rc и прочие возможные (например *.idl). Пока что я не пришел к определенному мнению на этот счет. Дело в том, что здесь надо как-то балансировать между удобством обращения с проектом - как с большими файлами, так и с большим количеством файлов - и объемом перекомпилируемого кода. Несомненно, описание, которое используются более чем в одном месте, не должно находиться в разных файлах: сделав изменение в одном немудрено забыть изменить второй - ошибка не видна на этапе компиляции, но иногда довольно трудно различима на этапе отладки.
Конечно же, "разбиение проекта" - это не только файлы, а еще и модули, функции, классы, компоненты, интерфейсы и т.д. и т.п. И все это - элементики, на которые надо разбивать проект. А все это разбиение - это и есть проектирование. Так сказать, разделяй и властвуй.
Warning - Обращайте на них внимание! Пердупреждение на этапе компиляции - это потенциальная ошибка во время исполнения!
Так я получил такое сообщение:
warning C4129: 'p' : unrecognized character escape sequence
- Хм..., - подумал я, - и что бы это значило? причем таких шесть штук! Присмотревшись, я нашел старый добрый глюк: косые слэши не были проставлены по два, что воспринималось компилятором как совершенно немыслимые Escape-последовательности...
Как проверить, состоит ли строка только из символов определенного множества? Я "потыкал пальчиком" в MFC и у меня получилось вот так вот:
str1 == str1.SpanIncluding(_T("0123456789ABCDEF")
Если str1 не содержит ничего лишнего, результатом этого выражения будет истина. Т.е. строки будут равны. Наверное это не слишком производительно, но это предназначено для работы в пользовательском интерфейсе - так уж и быть, парой-другой сотых секунды (тысячных? меньше?) можно пренебречь.
Для элемента управления IPAddress в MFC нет функции обмена данными со строкой. Есть только класс-оболочка - CIPAddressCtrl. В общем-то этот класс все делать и позволяет, но мне, почему-то, оказалось более удобным написать следующую функцию:
void AFXAPI DDX_IPAddress(CDataExchange* pDX, int nIDC, CString& value)
{
HWND hWndCtrl = pDX->PrepareEditCtrl(nIDC);
if (pDX->m_bSaveAndValidate)
{
DWORD dwAddress;
::SendMessage(hWndCtrl, IPM_GETADDRESS, 0, (LPARAM) &dwAddress);
value.Format(_T("%u.%u.%u.%u"),
FIRST_IPADDRESS(dwAddress),
SECOND_IPADDRESS(dwAddress),
THIRD_IPADDRESS(dwAddress),
FOURTH_IPADDRESS(dwAddress));
}
else
{
if (value.IsEmpty())
::SendMessage(hWndCtrl, IPM_CLEARADDRESS, 0, 0);
else
{
DWORD dwAddress = inet_addr(value);
// swap bytes
DWORD dwSwapped =
( (dwAddress & 0xFF000000) >> 24 ) +
( (dwAddress & 0x00FF0000) >> 8 ) +
( (dwAddress & 0x0000FF00) << 8 ) +
( (dwAddress & 0x000000FF) << 24 );
::SendMessage(hWndCtrl, IPM_SETADDRESS, 0, (LPARAM) dwSwapped);
}
}
}
А это пример ее использования:
void CSomeDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CSomeDlg)
DDX_IPAddress(pDX, IDC_IPADDRESS, m_strAddress);
...
//}}AFX_DATA_MAP
}
Функций проверок DDV_... для этого элемента управления пока не реализовывалось.
Своеобразная ловушка в параметрах по умолчанию, доступных в C++:
BOOL CLevelPage::OnInitDialog()
{
CPropertyPage::OnInitDialog();
m_cLevelCtrl.SetRange(0, 3);
...
и
BOOL CLevelPage::OnInitDialog()
{
CPropertyPage::OnInitDialog();
m_cLevelCtrl.SetRange(0, 3, TRUE);
...
Эти варианты отличаются довольно незаметно, но приводят к довольно заметной разнице в результате: в первом случае после отображения диалогового окна положение бегунка оказывается совершенно не таким, каким должно быть - оно явно тяготеет к нижней (левой) части шкалы. Причем по делениям не попадает. Хм... Мало того, оно еще и скачет при попытке пользователя работать с бегунком! Я никак не мог понять, в чем же тут дело, и оставлял эту маленькую проблемку на этам "вылизывания интерфейса". Теперь вот "пришло время собирать камни" и я начал копать.
А ларчик просто открывался: CLevelPage::DoDataExchange(...) выполняется где-то во время вызова родительской CPropertyPage::OnInitDialog() и при этом происходит инициализация Slider Control и установка его положения. А вот CSliderCtrl::SetRange(...) я зову уже позже и при этом не указываю последний параметр - BOOL bRedraw = FALSE - а по умолчанию то он FALSE! Т.е. элемент управления то не перерисовывается. И поэтому положение ползунка "лежит" в нижней части шкалы. И поэтому же оно "скачет" в правильное положение под действием пользователя. В общем, параметры по умолчанию - это, конечно же, красиво и удобно, но внимательность для програмиста все еще далеко не самое последнее качество...
Редактор ресурсов диалоговых окон MS Visual C++ 6.0 содержит в себе такие удобные штуки, как "поля" (margins) и "направляющие" (guides). Первые - это такая синяя рамка по краю диалогового окна - она есть постоянно, по крайней мере по-умолчанию. Вторые - это горизонтальные и вертикальные линии того же синего цвета, которые можно "вставить" в любое нужное место. Элементы управления "липнут" своими границами к этим линиям, что позволяет выстраивать их не "на глазок", а "так как доктор прописал". (тут у меня просто не хватает словарного запаса русского языка...)
Направляющие (guides) я пока оставлю в стороне - я ими не так часто пользуюсь и работа с ними мало отличатся. А вот поля (margins) иногда создают проблему: если к нижнему полю "прилепить" один или несколько элементов, то при изменении размеров диалога они едут вместе с полем. Так и должно быть, разумеется. Однако тут есть одно неудобство: а что делать, когда я не хочу, чтобы так было? Например, этот "прилепленный" элемент - это рамка, которая выделяет н-ное количество других "контролов" и логически связана с ними. Рамка то "поедет", а вот элементики внутри нее - нет. Абыдна, правда?
Хитрость в чем? Хитрость в том, чтобы с минимальными потерями "оторвать" нужный элемент или элементы от поля. Для этого надо:
Все - поле свободно! Его можно "совать" куда угодно и как угодно! Только после "сования" не забудьте:
...
А на этом, собственно, и все: с MS Visual C++ 6.0 я больше не работаю - теперь я работаю с MS Visual C++ 7.1, aka Visual Studio .NET 2003...
|
|