The Russian Electronic Developer Magazine | |
Русский электронный журнал разработчика | |
Издание третье, исправленное и дополненное.
Замечание 1. | Следует отметить, что данная публикация является попыткой автора осмыслить
и связно изложить некоторые известные (может быть :)) проблемы и способы
их решения для одного не очень внятно излагаемого вопроса программирования
под OS/2. |
Замечание 2. | Автор не считает, что предлагаемые им решения являются единственно правильными. Тем не менее, все предлагаемые решения достаточно строго обоснованы. |
Итак, вы начали разрабатывать свою новую программу и уже придумали внешний вид главного окна, продумали элементы управления и систему меню, а также использование клавиш-акселераторов. И тут вы отмечаете, что главное окно вашей программы поразительно похоже на окно диалога. И что было бы очень даже неплохо воспользоваться средствами разработки диалогов (dlgedit или другими) вместо достаточно скучного программирования размещения элементов управления в обычном окне. Это, кроме всего прочего, позволило бы хранить шаблон диалога в ресурсах и, тем самым, сделало бы программу независимой от формы диалога. А если следовать рекомендациям IBM и собрать в ресурсы все текстовые строки, то это существенно облегчит дальнейшие работы по переводу интерфейса вашей программы на другие языки. Например, тогда для создания англоязычного варианта програмы достаточно будет перевести на английский необходимые текстовые строки в исходных файлах ресурсов и заменить ресурсы в исполняемом модуле, воспользовавшись компилятором ресурсов rc. Перекомпиляция самой программы не потребуется.
Но может ли диалог быть главным окном приложения? Сохранятся ли при этом привычные атрибуты и свойства главного окна приложения, такие как собственный значок приложения, пользовательское меню, клавиши-акселераторы и т.д.? Чтобы попытаться понять и ответить на эти вопросы, обратимся к тому, как трактуется понятие "диалог (dialog box)" в Presentation Manager:
Цитата из RedBooks "OS/2 V2.0 Volume 3: PM and Workplace Shell"Из всего этого следует, что изначально не предполагалось, что диалог можно будет использовать в качестве главного окна приложения. Однако, если от использования модального диалога приходится отказаться именно в силу его модальности (кроме всего прочего, абсолютно не ясны вопросы минимизации и восстановления и то, как поведет себя, например, модальный диалог при вызове из другого модального диалога), то применение немодального диалога сдерживает только запрет на изменение размеров окна диалога пользователем, вытекающий из требований обеспечения целостности диалога ("оптимизированные окна").
Подраздел "Dialog Box"Диалог (dialog box) - это окно специального типа для использования в определенных обстоятельствах, когда требуется дополнительный обмен данными с пользователем. Диалоги могут быть двух отдельных типов - модальные и немодальные.
Диалог создается как коротко живущее окно для получения и/или отображения некой специфичной информации, необходимой для правильного выполнения приложения.
В модальном диалоге пользователь не может взаимодействовать с другими окнами до тех пор, пока диалог не будет закрыт. Это свойство может распространяться как на отдельное приложение (в этом случае невозможно общение с другими окнами того же самого приложения; общение же с другими приложениями в системе не запрещено), так и на всю систему (в этом случае запрещено общение с любыми другими окнами в системе до завершения диалога).
В немодальном диалоге пользователь может остановиться и продолжить работу, например, в первичном окне приложения (как это сделано для окна "Поиск" в OS/2 System Editor). Немодальный диалог предпочтительнее с позиции CUA.
Еще одно различие между диалогом и стандартным окном состоит в том, что размеры диалога не могут быть изменены (но тем не менее в определенных обстоятельствах он может быть минимизирован или максимизирован). Это свойство диалога является критичным в обеспечении целостности общения с конечным пользователем. Отметим, что хоть и нельзя менять размеры диалога, но перемещать его по рабочему столу можно.
Цитата из Redbooks "OS/2 V2.0 Volume 4: Application Development"
Подраздел "Action Windows"По правилам CUA'91, диалог должен называться окном действия (action window). Поскольку в этой главе обсуждаются правила CUA, то термин "окно действия" будет использоваться в отношении положений, разработанных в CUA. Термин "диалог" будет использоваться в отношении применения этих концепций для Presentation Manager.
Окна действия, используемые приложениями, могут быть как модальными, так и немодальными, хотя немодальные окна предпочтительнее с позиции CUA. Окна действия обоих типов определены в CUA, как оптимизированные окна. Таким образом, они создаются предопределенного оптимального размера для своих функций и эти размеры не могут быть изменены пользователем. Конечно, использование каждого типа окон действия различно, как показано ниже.
Диалог обычно разрабатывается с помощью Dialog Editor, поставляемым в составе IBM Developer's Toolkit for OS/2, который позволяет создать шаблон диалога и сохранить его в .DLG-файле. В скрипт-файле ресурсов приложения должна существовать ссылка на этот файл (директива rcinclude).
Рекомендуется всегда, когда только возможно, создавать окна действия с заголовком. Поскольку обычно окна действия показываются в результате выбора какого-либо пункта меню, то заголовок должен содержать наименование родительского объекта и название пункта меню. Так пользователь сможет получить информацию о действиях, приведших к появлению этого окна.
Как уже отмечалось, размеры окна действия не могут быть изменены пользователем, но он может перемещать его. Модальное окно действия также не должно содержать значков минимизации или максимизации, поскольку пользователь должен завершить работу с окном действия для того, чтобы продолжить выполнение приложения. Для немодального окна действия нет смысла в предоставлении значка максимизации, поскольку окно имеет оптимальный размер для той информации, которую оно содержит. Тем не менее, поскольку пользователь может захотеть прервать диалог для работы с другими окнами, то он должен иметь возможность убрать окно действия с рабочего стола. В таком случае, немодальное окно действия должно иметь значок минимизации.
Цитата из Redbooks "OS/2 V2.0 Volume 4: Application Development"
подраздел "Modeless Action Windows"Немодальное окно действия предпочтительнее в ситуации, когда может возникнуть необходимость общения с другими окнами приложения до завершения работы с диалогом.
Поскольку функция WinDlgBox() автоматически создает и исполняет модальный диалог, то немодальный диалог можно создать одним из двух способов:
Рекомендуется использовать второй способ по причинам простоты, поскольку диалог и его управляющие окна могут быть определены и сохранены в файле ресурсов, что облегчает разработку диалога для программиста.
- использовать стандартную функцию WinCreateWindow() с установленным флагом создания фрейма FCF_BORDER. Управляющие окна, такие как поля ввода и кнопки, могут быть созданы как дочерние окна,
- использовать вызов WinLoadDlg() для загрузки шаблона диалога из ресурсов в память, а затем вызвать функцию WinShowWindow() или WinSetWindowPos(), чтобы сделать диалог видимым. Шаблон диалога должен иметь атрибут FCF_BORDER. Управляющие окна в таком диалоге описываются в шаблоне диалоге.
Диалог может быть размещен в заданных координатах экрана, независимо от используемого метода. В первом случае, при использовании WinCreateWindow(), диалог позиционируется во время создания. Во втором случае, диалог позиционируется во время обработки сообщения WM_INITDLG.
Результатом применения атрибута FCF_BORDER при создании диалога будет прорисовка тонкой синей линии вокруг окна (бордюр). Это соответствует правилам SAA CUA для немодальных окон действия.
Отметим, что два вышеописанных метода получают различные сообщения инициализации. Созданный функцией WinCreateWindow() диалог будет рассматриваться как "обычное" окно и ему будет передано сообщение WM_CREATE. Для диалога, созданного функцией WinDlgBox(), будет использовано сообщение WM_INITDLG. Разработчик приложения должен учитывать это при написании процедуры обработки диалога.
Этот запрет можно просто игнорировать, если не давать пользователю возможности изменять размеры главного окна или предпринять собственные меры для обеспечения целостности диалога.
Приведем пример того, как можно построить приложение, использующее диалог в качестве главного окна.
Допустим, что в главном окне нашего приложения (назовем его "Болван диалоГовый" - коротко БГ) должны размещаться три управляющих элемента (текстовое окно, значок приложения и кнопка "Выход"). Кроме того, мы хотим использовать меню и клавиши-акселераторы (например, F3 - выход):
И, конечно же, мы хотим, чтобы пользователь мог самостоятельно изменять размеры главного окна нашего приложения (как у любого нормального окна), в том числе, и минимизировать, и максимизировать его. А сохранение позиции и размеров окна по окончании работы и восстановление их при последующем запуске вполне соответствовало бы правилам хорошего тона.
Для того, чтобы обеспечить целостность диалога, условимся о том, что размеры окна диалога не могут быть меньше некоторых минимальных значений (исключая минимизацию), и что при изменении размеров окна диалога кнопка "Выход" всегда будет располагаться в правом нижнем углу окна, а значок приложения -- в правом верхнем. Размеры текстового окна будут изменяться в соответствии с изменением размеров окна диалога. Минимальными размерами диалога условимся считать размеры окна, создаваемого системой при загрузке диалога из ресурсов.
Создадим скрипт-файл ресурсов (boldy.rc):
#include <os2.h> #include "boldy.h" ICON ID_MAIN boldy.ico ACCELTABLE ID_MAIN PRELOAD BEGIN VK_F3, ID_EXIT, VIRTUALKEY END MENU ID_MAIN PRELOAD BEGIN SUBMENU "Файл", ID_FILE BEGIN MENUITEM "Откpыть", ID_OPEN, MIS_TEXT MENUITEM "Сохpанить", ID_SAVE, MIS_TEXT MENUITEM "Сохpанить как ...", ID_SAVEAS, MIS_TEXT MENUITEM SEPARATOR MENUITEM "Выход\tF3", ID_EXIT, MIS_TEXT END SUBMENU "Справка", ID_HELP BEGIN MENUITEM "О пpогpамме...", ID_ABOUT, MIS_TEXT END END STRINGTABLE PRELOAD BEGIN IDS_CREATEMAIN, "Ошибка создания главного окна приложения" IDS_APPTITLE, "Болван диалоГовый" END rcinclude boldy.dlgНикаких особенностей в этом скрипт-файле нет. Отметим только, что некоторые ресурсы (иконка, таблица акселераторов и меню) имеют один и тот же идентификатор ID_MAIN. Если бы мы для создания главного окна приложения с идентификатором ID_MAIN использовали функцию WinCreateStdWindow() и при этом установили бы флаги FCF_ICON, FCF_ACCELTABLE и FCF_MENU, то для созданного окна (фрейма) автоматически были бы загружены и использованы все эти ресурсы. Кроме того, установка флага FCF_TASKLIST привела бы к тому, что наше главное окно сразу же появилось бы в списке окон.
Теперь нарисуем шаблон диалога (boldy.dlg):
DLGINCLUDE 1 "boldy.h" DLGTEMPLATE ID_MAIN LOADONCALL MOVEABLE DISCARDABLE BEGIN DIALOG "", ID_MAIN, 0, 0, 156, 43, NOT FS_DLGBORDER | FS_SIZEBORDER | NOT WS_SAVEBITS, FCF_SYSMENU | FCF_TITLEBAR | FCF_MINBUTTON | FCF_MAXBUTTON BEGIN DEFPUSHBUTTON "Выход", ID_EXIT, 98, 0, 58, 14 ICON ID_MAIN, ID_ICON, 117, 20, 20, 16, WS_GROUP LTEXT "Этот простой пример демонстрирует возможность созда" "ния диалоговых окон с изменяемыми размерами и обычн" "о отсутствующими атрибутами фрейма: (1)собственный " "значок приложения, (2)пользовательское меню, (3)кла" "виши-акселераторы.", ID_TEXT, 1, 0, 90, 41, DT_WORDBREAK PRESPARAMS PP_BACKGROUNDCOLOR, 0x00FFFFDCL PRESPARAMS PP_FONTNAMESIZE, "8.Helv" END ENDПоскольку диалог будет использоваться в качестве главного окна приложения, то ему тоже присвоен идентификатор ID_MAIN. Отметим сразу, что в описании шаблона диалога отсутствуют флаги FCF_ICON, FCF_MENU, FCF_ACCELTABLE и FCF_TASKLIST. Дело в том, что эти флаги игнорируются функциями создания окна диалога. Поэтому, независимо от того, установлены эти флаги или нет, замена вызова функции WinCreateStdWindow() на WinLoadDlg() приведет к тому, что:
Кроме того, в шаблоне диалога не используется флаг WS_VISIBLE. Это значит, что окно диалога не будет отображаться на экране до тех пор, пока мы не сделаем его видимым с помощью функций WinShowWindow() или WinSetWindowPos().
Из всего вышеизложенного следует, что нам придется самим подгружать указанные ресурсы и инициализировать их использование в программе.
Посмотрим, как это можно сделать (мы не будем рассматривать полный текст boldy.c, а только выделим и обсудим основные моменты). Как уже говорилось ранее, в качестве главного окна приложения можно использовать немодальный диалог, создав его с помощью функции WinLoadDlg(). Некоторые ресурсы (заголовок окна и иконку) можно загрузить прямо в главной процедуре приложения сразу после создания очереди сообщений:
. . . // Загружаем строку с заголовком окна WinLoadString( hab, (HMODULE)0L, IDS_APPTITLE, MAXNAMEL, (PSZ)szAppTitle ); // Загружаем икону приложения hptrIcon = WinLoadPointer( HWND_DESKTOP, 0L, ID_MAIN ); // Создаем главное окно приложения hwndMain = WinLoadDlg( HWND_DESKTOP, // где разместить HWND_DESKTOP, // кто собственник WndDlgProc, // процедура обработки диалога (HMODULE)0, // 0 - в EXE #0 - в DLL ID_MAIN, // идентификатор диалога в ресурсе NULL); // данные для инициализации . . .Поскольку мы использовали функцию WinLoadDlg(), то процедура обработки при инициализации окна получит сообщение WM_INITDLG, а не WM_CREATE. В принципе, можно было бы загрузить и инициализировать недостающие ресурсы при обработке этого сообщения. Но следует учитывать, что на момент его поступления окно еще фактически не существует. Поэтому, например, не удастся создать пользовательское меню или узнать размеры окна. Чтобы обойти это ограничение, обработчик сообщения WM_INITDLG может, например, поставить в очередь какое-нибудь сообщение пользователя (MY_QUERYSIZE в нашем примере), обработка которого начнется после завершения процедуры инициализации окна:
case WM_INITDLG: WinPostMsg( hwnd, MY_QUERYSIZE, NULL, NULL ); return (MRESULT)TRUE;Обратите внимание, что обработчик сообщения WM_INITDLG возвращает значение TRUE. В этом случае, по завершении процесса инициализации диалога система не будет пытаться установить фокус на окно диалога. А нам это и не надо, поскольку мы все равно будем сами активировать свое окно.
Пользовательские сообщения MY_QUERYSIZE и MY_SETSIZE (оно нам потребуется позже) определены в файле заголовков boldy.h следующим образом:
#define MY_QUERYSIZE WM_USER+1 #define MY_SETSIZE WM_USER+2Сообщение MY_QUERYSIZE поступит на обработку, когда процедура инициализации окна будет выполнена полностью. К этому времени окно уже полностью создано, но еще отсутствует на рабочем столе (при создании окна не использовался флаг WS_VISIBLE). Размеры этого окна определяются системой по неким правилам, исходя из размеров шаблона диалога и действующего разрешения экрана. Теперь мы можем загрузить и инициализировать недостающие ресурсы:
case MY_QUERYSIZE: { SWP swp; SWCNTRL swctl; HSWITCH hswitch; HACCEL hAccel; FRAMECDATA frame; // Установим собственную иконку приложения WinSendMsg( hwndMain, WM_SETICON, (MPARAM)hptrIcon, (MPARAM)0L ); // Установим заголовок окна WinSetWindowText( hwndMain, szAppTitle ); // Загрузим и инициируем таблицу акселераторов hAccel = WinLoadAccelTable( hab, (HMODULE)NULLHANDLE, ID_MAIN ); WinSetAccelTable( hab, hAccel, hwndMain ); // Добавим окно в список окон swctl.hwnd = hwndMain; swctl.hwndIcon = hwndMain; swctl.hprog = NULLHANDLE; swctl.idProcess = 0; swctl.idSession = 0; swctl.uchVisibility = SWL_VISIBLE; swctl.fbJump = SWL_JUMPABLE; swctl.szSwtitle[0] = 0x00; swctl.szSwtitle[MAXNAMEL+1] = 0x00; strncpy( swctl.szSwtitle, szAppTitle, MAXNAMEL ); hswitch = WinAddSwitchEntry( &swctl ); // Создадим пользовательское меню frame.cb = sizeof( FRAMECDATA ); frame.flCreateFlags = FCF_MENU; frame.hmodResources = 0L; frame.idResources = ID_MAIN; WinCreateFrameControls( hwndMain, &frame, (PSZ)0L );Поскольку вновь созданное пользовательское меню может перекрыть некоторые управляющие окна диалога, то окно, создаваемое системой по умолчанию, надо увеличить по вертикали на высоту окна меню. После этого можно сохранить размеры окна (как мы условились раньше, это будет окно минимально возможных размеров). Флаг SWP_NOADJUST запрещает передачу сообщения WM_ADJUSTWINDOWPOS (здесь нам не требуется выравнивание элементов диалога):
WinQueryWindowPos( hwnd, (PSWP)&swp ); swp.cy += WinQuerySysValue( HWND_DESKTOP, SV_CYMENU ); swpDefault = swp; swpMini = swp; WinSetWindowPos( hwnd, HWND_TOP, 0, 0, swp.cx, swp.cy, SWP_SIZE | SWP_NOADJUST );Тут есть одна тонкость: если просто создать пользовательское меню и не изменять размеры окна (скажем, размеры меню учли при создании шаблона диалога), то пользовательское меню в окне не появится. Оно выскочит только тогда, когда пользователь начнет изменять размеры окна. :-?
Мы уже говорили о правилах хорошего тона и о том, что приложение должно
уметь сохранять и восстанавливать позицию и размеры своего окна.
Проще всего для этого использовать пару функций
WinStoreWindowPos() / WinRestoreWindowPos().
Эти функции
if ( !WinRestoreWindowPos( (PSZ)szAppName, (PSZ)szKeyName, hwndMain ) ) WinPostMsg( hwnd, MY_SETSIZE, MPFROMLONG(swpDefault.cx), MPFROMLONG(swpDefault.cy) ); } break;Сообщение MY_SETSIZE предназначено для информирования приложения об изменении размеров окна пользователем. В качестве параметров оно получает предыдущие размеры окна (т.е. те размеры, которые окно имело перед изменением размеров). Обработчик сообщения занимается тем, что определяет величину изменения размеров окна и позицинирует (и/или изменяет размеры) элементы диалога в окне нового размера, а затем показывает окно диалога на экране с помощью функции WinShowWindow().
case MY_SETSIZE: { SWP swp; LONG deltaX, deltaY; WinSetWindowPos( hwnd, HWND_TOP, 0L, 0L, 0L, 0L, SWP_ACTIVATE ); WinQueryWindowPos( hwnd, (PSWP)&swp ); if ( !(swp.fl & SWP_MINIMIZE) ) { deltaX = swp.cx - LONGFROMMP(mp1); deltaY = swp.cy - LONGFROMMP(mp2); if ( deltaX!=0 || deltaY!=0 ) SetDlgItemPos( deltaX, deltaY ); WinShowWindow( hwnd, TRUE ); } } break;Вызов функции WinSetWindowPos( ..., SWP_ACTIVATE ) позволяет решить одну проблему, возникающую из-за неадекватного поведения системы при максимизации окна из минимизированного состояния. Дело в том, что функция WinQueryWindowPos() будет возвращать размеры окна равные размеру миниокна (42x40 для иконки 32x32), несмотря на то, что на экране уже прорисовано окно новых размеров. Ситуация усугубляется еще тем, что простого включения флага SWP_ACTIVATE не достаточно. Функция WinSetWindowPos( ..., SWP_ACTIVATE ) не будет выполнять никаких действий, если окно уже находится в активном состоянии. Поэтому необходимо сначала сбросить флаг SWP_ACTIVATE, а потом снова включить его.
Особенность обработки диалогов состоит в том, что при изменении размеров окна для уведомления используется сообщение WM_ADJUSTWINDOWPOS, а не WM_SIZE. И сброс флага SWP_ACTIVATE лучше делать при обработке этого сообщения. Посмотрим, как устроен обработчик сообщения:
case WM_ADJUSTWINDOWPOS: { SWP swp; WinQueryWindowPos( hwnd, (PSWP)&swp );Если запрошена минимизация окна, то достаточно просто сохранить текущие размеры окна.
if (((PSWP)mp1)->fl & SWP_MINIMIZE) WinQueryWindowPos( hwnd, (PSWP)&swpMini );Если запрошено восстановление или максимизация окна, то необходимо сначала проверить не находится ли окно в минимизированном состоянии. Если - да, то обязательно требуется сбросить флаг SWP_ACTIVATE.
else if ( (((PSWP)mp1)->fl & SWP_RESTORE) || (((PSWP)mp1)->fl & SWP_MAXIMIZE) ) { if ( swp.fl & SWP_MINIMIZE ) { ((PSWP)mp1)->fl = ((PSWP)mp1)->fl & ~SWP_ACTIVATE; WinPostMsg( hwnd, MY_SETSIZE, MPFROMLONG(swpMini.cx), MPFROMLONG(swpMini.cy) ); } else { if ( (((PSWP)mp1)->cx<swpDefault.cx) || (((PSWP)mp1)->cy<swpDefault.cy) ) { ((PSWP)mp1)->cx = max( swpDefault.cx, ((PSWP)mp1)->cx ); ((PSWP)mp1)->cy = max( swpDefault.cy, ((PSWP)mp1)->cy ); } WinPostMsg( hwnd, MY_SETSIZE, MPFROMLONG(swp.cx), MPFROMLONG(swp.cy) ); } }Изменение размеров окна необходимо контролировать, чтобы сохранить целостность диалога. Размеры окна не могут быть меньше некоторых минимальных значений, за которые мы приняли размеры окна диалога, создаваемого при загрузке диалога из ресурсов. Размеры окна надо контролировать как у обычного окна, так и у максимизированного.
else if (((PSWP)mp1)->fl & SWP_SIZE) { if ( (((PSWP)mp1)->cx<swpDefault.cx) || (((PSWP)mp1)->cy<swpDefault.cy) ) { ((PSWP)mp1)->cx = max( swpDefault.cx, ((PSWP)mp1)->cx ); ((PSWP)mp1)->cy = max( swpDefault.cy, ((PSWP)mp1)->cy ); } WinPostMsg( hwnd, MY_SETSIZE, MPFROMLONG(swp.cx), MPFROMLONG(swp.cy) ); } } return WinDefDlgProc( hwnd, msg, mp1, mp2 );И последнее, на что стоит обратить внимание, это обработка сообщения WM_SAVEAPPLICATION. Именно здесь выполняется сохранение параметров окна:
case WM_SAVEAPPLICATION: WinStoreWindowPos( (PSZ)szAppName, (PSZ)szKeyName, hwnd ); return WinDefDlgProc( hwnd, msg, mp1, mp2 );В предыдущих вариантах программы БГ (Болван диалоГовый) для этих целей использовалось сообщение WM_CLOSE. Но это не совсем правильно, так как оно генерируется только в тех случаях, когда пользователь закрывает приложение двойным щелчком по минизначку, щелчком по кнопке закрытия в полосе заголовка (версия >= Warp3+fix29), из системного меню и обзора миниокон. Если же приложение закрывается из списка окон (и, естественно, при закрытии системы), то сообщение WM_CLOSE не генерируется, в отличие от сообщения WM_SAVEAPPLICATION.
В результате мы получаем программу, которая:
Полный текст программы БГ (Болван диалоГовый) находится в архиве boldy3.zip. Единственное, на что стоит еще обратить внимание -- то, что при сборке этой программы не стоит делать размер стека меньше 16K.
Успехов,
VicTor Smirnoff
Интересные ссылки:
Большой портал для малого бизнеса.
Комментариев к странице: 0 | Добавить комментарий
Редактор: Дмитрий Бан
Оформление: Евгений Кулешов