Руководство программиста
Глава 2. Создание моделей блоков
§2.12. Реакция блоков на действия пользователя
§2.12.3. Реакция на мышь в блоках сложной формы
Рассматриваются особенности реакции на мышь в перекрывающихся блоках сложной формы, в которых часть изображения одного блока просматривается сквозь изображение другого. Приводится пример блока с «отверстием», которое может открываться и закрываться, разрешая или запрещая тем самым щелчки мышью по полю ввода, видимому в отверстии.
Модель блока, если в его параметрах разрешена реакция на мышь, получает информацию о нажатии и отпускании кнопок и перемещении курсора, если курсор находится в пределах описывающего прямоугольника изображения блока (то есть прямоугольника с минимальной шириной и высотой, который можно описать вокруг изображения блока). Однако, довольно часто изображения блоков имеют сложную форму, и, если блоки в подсистеме перекрываются, в описывающий прямоугольник такого изображения могут попасть части изображений других блоков. При этом пользователь, щелкая мышью по ясно видимому в окне изображению, может не подозревать, что на самом деле это изображение перекрыто описывающим прямоугольником какого-либо другого блока, который перехватит этот щелчок.
Рис. 76. Перекрывающиеся блоки
Например, если блок 2 со сложным изображением на рис. 76 расположен на переднем плане, его описывающий прямоугольник (изображен пунктиром) будет перекрывать прямоугольник блока 1. Пользователь, щелкая мышью по изображению блока 1, будет ожидать реакции на этот щелчок именно от этого блока. Однако, поскольку блок 2 с его прямоугольником располагается поверх блока 1, информацию о нажатии кнопки мыши получит блок 2. Если в параметрах блока 2 не разрешена реакция на мышь, RDS не будет вызывать его модель и вызовет модель следующего по близости к переднему плану блока, то есть блока 1, что соответствует замыслу пользователя. Однако, если блок 2, как и блок 1, реагирует на мышь, будет вызвана именно его модель, что будет для пользователя полной неожиданностью, ведь он щелкал по ясно видимому изображению блока 1. Чтобы этого не происходило, модели сложных по форме блоков должны анализировать, не пришелся ли щелчок мыши на прозрачную (не занятую изображением) область описывающего прямоугольника и отказываться реагировать на мышь в таких случаях. Для этого функция модели, вызванная для реакции на мышь, должна вернуть константу RDS_BFR_NOTPROCESSED вместо обычной RDS_BFR_DONE – это укажет RDS на то, что для реакции на мышь нужно вызвать модель другого блока.
Рис. 77. Подключение блока управления
полем ввода
В качестве примера создадим блок, который можно использовать для управления полем ввода. Блок будет иметь два вещественных входа: «x_ext» и «x_int», вещественный выход «out» и внутреннюю логическую переменную «bypass». У блока будет два состояния: открытое и закрытое. В открытом состоянии («bypass»=0) блок будет выглядеть как рамка зеленого цвета с прозрачным окном внутри, при этом он будет передавать на выход «out» значение входа «x_int». В закрытом состоянии («bypass»=1) блок будет сплошным красным прямоугольником, внутри которого отображается значение выхода «out», при этом на выход должно передаваться значение входа «x_ext». Состояния блока будут переключаться по щелчку мыши. Если разместить этот блок поверх поля ввода, подключенного к его входу «x_int», блок сможет управлять этим полем ввода. В открытом состоянии поле ввода будет видно в окне в центре блока, и на выход блока будет передаваться значение, введенное в поле (чтобы пользователь смог работать с полем ввода, необходимо написать модель блока так, чтобы она не реагировала на щелчки мыши внутри прозрачного окна). В закрытом состоянии блок будет перекрывать поле ввода, и, вместо его значения, будет передавать на выход значение с входа «x_ext». На рис. 77 изображен пример подключения такого блока к полям ввода: в открытом состоянии на числовой индикатор будет выдаваться значение поля «A2», в закрытом – «A1». Выход блока «out» на рисунке подключен еще и ко входу «v» поля ввода «A2», чтобы в закрытом состоянии, когда значение «out» совпадает с «x_ext», это поле автоматически получало значения с поля «A1».
Блок управления будет иметь следующую структуру переменных:
| Смещение | Имя | Тип | Размер | Вход/выход | Пуск | Начальное значение |
|---|---|---|---|---|---|---|
| 0 | Start | Сигнал | 1 | Вход | ✓ | 1 |
| 1 | Ready | Сигнал | 1 | Выход | 0 | |
| 2 | x_ext | double | 8 | Вход | ✓ | 0 |
| 10 | x_int | double | 8 | Вход | ✓ | 0 |
| 18 | out | double | 8 | Выход | 0 | |
| 26 | bypass | Логический | 1 | Внутренняя | 0 |
Модель блока должна запускаться только при поступлении на его входы новых значений, поэтому в параметрах блока нужно включить запуск по сигналу и установить флаг «» для всех входов блока. Сама модель будет иметь следующий вид:
extern "C" __declspec(dllexport) int RDSCALL EditControlFrame(int CallMode, RDS_PBLOCKDATA BlockData, LPVOID ExtParam) { // Макроопределения для статических переменных #define pStart ((char *)(BlockData->VarTreeData)) #define Start (*((char *)(pStart))) #define Ready (*((char *)(pStart+RDS_VSZ_S))) #define x_ext (*((double *)(pStart+2*RDS_VSZ_S))) #define x_int (*((double *)(pStart+2*RDS_VSZ_S+RDS_VSZ_D))) #define out (*((double *)(pStart+2*RDS_VSZ_S+2*RDS_VSZ_D))) #define bypass (*((char *)(pStart+2*RDS_VSZ_S+3*RDS_VSZ_D))) const int fr=20; // Толщина рамки // Вспомогательные переменные RDS_PMOUSEDATA mouse; RDS_PDRAWDATA draw; int frz,x1,y1,x2,y2,xi1,yi1,xi2,yi2; switch(CallMode) { // Проверка типов статических переменных case RDS_BFM_VARCHECK: return strcmp((char*)ExtParam,"{SSDDDL}")? RDS_BFR_BADVARSMSG:RDS_BFR_DONE; // Реакция на нажатие кнопки мыши case RDS_BFM_MOUSEDOWN: mouse=(RDS_PMOUSEDATA)ExtParam; // Толщина рамки с учетом масштаба frz=fr*mouse->DoubleZoom; // В открытом состоянии при попадании курсора внутрь // прозрачного окна на щелчок реагировать не нужно if(bypass==0 && mouse->x>mouse->Left+frz && mouse->y>mouse->Top+frz && mouse->x<mouse->Left+mouse->Width-frz && mouse->y<mouse->Top+mouse->Height-frz) return RDS_BFR_NOTPROCESSED; // Если не левая кнопка - не обрабатываем щелчок // и разрешаем вывести контекстное меню, если нужно if(mouse->Button!=RDS_MLEFTBUTTON) return RDS_BFR_SHOWMENU; // Нажата левая кнопка мыши, причем курсор попал // в рамку или блок в закрытом состоянии bypass=!bypass; // Переключаем состояние Ready=1; // Взводим сигнал готовности // Здесь намеренно не поставлен оператор break: необходимо // выполнить действия в следующем case (такт расчета) // Один такт расчета case RDS_BFM_MODEL: // В зависимости от состояния, подаем на выход // один из входов out=bypass?x_ext:x_int; break; // Рисование внешнего вида блока case RDS_BFM_DRAW: draw=(RDS_PDRAWDATA)ExtParam; // Координаты описывающего прямоугольника блока x1=draw->Left; x2=draw->Left+draw->Width; y1=draw->Top; y2=draw->Top+draw->Height; // Толщина рамки с учетом масштаба frz=fr*draw->DoubleZoom; // Координаты окна внутри блока xi1=x1+frz; xi2=x2-frz; yi1=y1+frz; yi2=y2-frz; if(bypass) { // Закрытое состояние int w; char *text; // Рисуем красный прямоугольник с черной рамкой rdsXGSetPenStyle(0,PS_SOLID,1,0,R2_COPYPEN); rdsXGSetBrushStyle(0,RDS_GFS_SOLID,0xff); rdsXGRectangle(x1,y1,x2,y2); // Устанавливаем шрифт выстой в окно внутри блока rdsXGSetFont(0,"Arial",yi2-yi1,0, DEFAULT_CHARSET,0,FALSE,FALSE,FALSE,FALSE); // Преобразуем значение выхода в динамическую строку text=rdsDtoA(out,-1,NULL); // Определяем ширину получившейся строки на экране rdsXGGetTextSize(text,&w,NULL); // Выводим значение выхода туда, где в открытом // состоянии находится прозрачное окно rdsXGTextOut(xi2-w,yi1,text); // Освобождаем динамическую строку rdsFree(text); } else { // Открытое состояние rdsXGSetBrushStyle(0,RDS_GFS_SOLID,0xff00); // Рисуем рамку вокруг прозрачного окна из четырех // зеленых прямоугольников rdsXGFillRect(x1,y1,x2,yi1); rdsXGFillRect(x1,yi2,x2,y2); rdsXGFillRect(x1,yi1,xi1,yi2); rdsXGFillRect(xi2,yi1,x2,yi2); // Обрамляем черными линиями rdsXGSetPenStyle(0,PS_SOLID,1,0,R2_COPYPEN); rdsXGSetBrushStyle(0,RDS_GFS_EMPTY,0); rdsXGRectangle(x1,y1,x2,y2); rdsXGRectangle(xi1,yi1,xi2,yi2); } break; } return RDS_BFR_DONE; // Отмена макроопределений #undef bypass #undef out #undef x_int #undef x_ext #undef Ready #undef Start #undef pStart } //=========================================
В такте моделирования (RDS_BFM_MODEL) выходу out присваивается значение одного из входов в зависимости от значения переменной состояния bypass – это самая простая реакция модели. Реакция на мышь и процедура рисования немного сложнее.
В реакции на нажатие кнопки мыши (RDS_BFM_MOUSEDOWN) модель сначала проверяет, не попал ли курсор в прозрачное окно внутри блока. Для этого вычисляется толщина рамки в текущем масштабе frz (толщина рамки в единичном масштабе задана как константа fr в начале функции). Затем, если блок в открытом состоянии (bypass==0), и курсор мыши находится далее чем в frz точках от его границ, функция немедленно возвращает константу RDS_BFR_NOTPROCESSED, информируя RDS о том, что щелчок пришелся в прозрачное окно и данная модель отказывается от его обработки. В противном случае щелчок пришелся на рамку или на прямоугольник блока в закрытом состоянии. Если была нажата не левая кнопка (нас интересуют только щелчки левой), функция возвращает константу RDS_BFR_SHOWMENU, информируя RDS о том, что, хотя модель и обработала нажатие кнопки, контекстное меню блока по правой кнопке вызвать все равно нужно (это стандартная практика при написании моделей блоков – если функция модели, обработав щелчок мыши, не хочет блокировать вызов контекстного меню блока, она должна вернуть RDS_BFR_SHOWMENU). Если же была нажата именно левая кнопка мыши, bypass инвертируется (состояние блока переключается), взводится сигнал готовности Ready для передачи выхода блока по связям в ближайшем такте расчета, и, из-за опущенного оператора break, выполняется следующая далее по тексту программы реакция на такт расчета, вычисляющая значение выхода блока по новому значению bypass.
При рисовании внешнего вида блока также вычисляется толщина рамки в текущем масштабе frz – она потребуется и в открытом состоянии блока (для рисования рамки вокруг прозрачного окна), и в закрытом (для вывода значения выхода блока туда, где в открытом состоянии через окно видно поле ввода). Кроме того, во вспомогательные переменные записываются вычисленные координаты описывающего прямоугольника блока x1, y1, x2, y2, и координаты прямоугольника окна внутри блока xi1, yi1, xi2, yi2 – они смещены внутрь блока на frz точек. Далее рисование производится по-разному для открытого и закрытого состояния.
В закрытом состоянии (bypass!=0) функция рисует красный прямоугольник размером с описывающий прямоугольник блока. Затем, при помощи функции rdsXGSetFont, устанавливаются параметры шрифта, которым будет выведено текущее значение выхода блока, причем в качестве высоты шрифта в функцию передается высота окна внутри блока yi2-yi1. Вещественное значение out преобразуется в динамически отведенную строку функцией rdsDtoA, после чего эта строка выводится на экран выровненной по правой границе внутреннего окна блока xi2. Затем память, отведенная под строку, освобождается функцией rdsFree, и рисование на этом завершается.
В открытом состоянии (bypass==0) необходимо нарисовать зеленый прямоугольник с прозрачным окном посередине. Поскольку ни в Windows API, ни в графических функциях RDS нет функции для рисования прямоугольника с отверстием, проще всего нарисовать рамку из четырех прямоугольников вокруг прозрачного окна, а затем, отключив заливку, отдельно нарисовать черные линии, ограничивающие блок и окно в его центре. Именно это и делается в модели блока.
Для проверки работы модели следует задать в параметрах блока программное рисование и разрешить ему реакцию на мышь. Затем следует расположить этот блок поверх поля ввода, подобрав его размер так, чтобы это поле было видно сквозь прозрачное окно в блоке, и провести соединения блока с этим полем и еще парой дополнительных блоков, как показано на рис. 77. Теперь, запустив расчет, можно подавать на числовой индикатор значение с «внешнего» или «внутреннего» полей ввода (рис. 78, связи между блоком управления и полем A2 убраны на невидимый слой), переключая состояние блока щелчками мыши. При этом, когда блок находится в открытом состоянии (зеленый цвет), поле ввода, видимое через прозрачное окно, должно реагировать на щелчки мыши, несмотря на то, что формально оно перекрыто блоком управления.

(а)

(б)
Рис. 78. Блок управления полем ввода в открытом (а) и закрытом (б) состоянии
Можно заметить, что у созданного блока есть один недостаток. В режимах моделирования и расчета все работает так, как и было задумано: щелчки в прозрачном окне блока не обрабатываются моделью, попадают в поле ввода и позволяют изменять его значение. Однако, в режиме редактирования модель блока на действия мышью не реагирует, поэтому, хотя поле ввода и видно через окно в прямоугольнике блока, выбрать его мышью, вызвать окно настроек или контекстное меню невозможно: все щелчки будут приходиться на лежащий выше блок. Для того, чтобы модель могла сделать блок «прозрачным» для щелчков и в режиме редактирования, в нее нужно включить реакцию на выбор блока мышью RDS_BFM_MOUSESELECT. Эта реакция очень похожа на реакцию на нажатие кнопки мыши в режимах моделирования и расчета: в параметре ExtParam также передается указатель на структуру RDS_MOUSEDATA, содержащую координаты курсора мыши, размеры описывающего прямоугольника блока, текущий масштаб и т.д., однако, она вызывается только в режиме редактирования. Если в ответ на этот вызов модель вернет RDS_BFR_DONE, точка будет считаться принадлежащей блоку, и RDS выполнит для него все обычные действия режима редактирования (выделит при щелчке левой кнопкой, откроет контекстное меню при щелчке правой, откроет окно параметров или настройки при двойном щелчке). Если же модель вернет RDS_BFR_NOTPROCESSED, RDS пропустит этот блок и будет искать другой, лежащий ниже, по этим же координатам.
Добавим в нашу модель реакцию на это событие. В оператор switch(CallMode) нужно добавить следующий case:
// Проверка возможности выбора блока мышью case RDS_BFM_MOUSESELECT: mouse=(RDS_PMOUSEDATA)ExtParam; frz=fr*mouse->DoubleZoom; // Толщина рамки // Проверка попадания в прозрачное окно if(bypass==0 && mouse->x>mouse->Left+frz && mouse->y>mouse->Top+frz && mouse->x<mouse->Left+mouse->Width-frz && mouse->y<mouse->Top+mouse->Height-frz) return RDS_BFR_NOTPROCESSED; // В окно не попали - функция вернет RDS_BFR_DONE break;
Новая реакция практически идентична реакции RDS_BFM_MOUSEDOWN, за исключением действий по переключению состояния блока – в режиме редактирования этого делать не нужно. При желании, можно совместить действия этих двух реакций, поставив их операторы case друг за другом.
Из-за похожести реакций на события RDS_BFM_MOUSEDOWN и RDS_BFM_MOUSESELECT у программиста может возникнуть искушение использовать последнее для реакции на щелчок мыши в режиме редактирования. Следует, однако, иметь в виду, что, независимо от того, какую кнопку нажмет пользователь на блоке при его выделении в режиме редактирования, в параметрах события RDS_BFM_MOUSESELECT будет указана левая. Это событие предназначено только для проверки возможности выделения блока щелчком мыши по указанной точке, и использовать его в других целях нежелательно.
В этом примере форма блока (прямоугольник с окном) задавалась программно. Таких же результатов можно достичь и с использованием векторных картинок: нужно каждому элементу картинки присвоить ненулевой идентификатор. Тогда, если функция rdsGetMouseObjectId (см. пример в §2.12.1) вернет нулевое значение, это даст модели понять, что курсор не попал ни в один из элементов картинки. Вернув в этом случае константу RDS_BFR_NOTPROCESSED, модель позволит отозваться на щелчок мыши какому-либо другому блоку, изображение которого видно под картинкой данного. При этом следует помнить, что проверка попадания курсора мыши в тот или иной векторный элемент картинки блока в функции rdsGetMouseObjectId также производится по описывающему прямоугольнику этого элемента. Например, если картинка блока 2 на рис. 76 будет представлять собой один Г-образный многоугольник, любой щелчок мышью в пределах описывающего прямоугольника этого многоугольника будет считаться попаданием в сам многоугольник, и блок 1 снова окажется перекрыт. Чтобы избежать этого, нужно либо составить картинку блока 2 из двух прямоугольников с ненулевыми идентификаторами, либо присвоить многоугольнику нулевой идентификатор (исключив его тем самым из проверки на попадание курсора мыши) и наложить на него два специальных прямоугольных векторных элемента «зона» (которые никак не отображаются на внешнем виде блока) с ненулевыми идентификаторами. Эти элементы специально предназначены для того, чтобы создавать в картинке блока зоны, чувствительные к нажатию кнопок мыши, не затрагивая внешний вид самой картинки. В данном случае, если покрыть такими элементами все непрозрачные детали картинки блока, цель будет достигнута: щелчок по видимым элементам картинки (на самом деле пришедшийся на добавленные «зоны») будет считаться попаданием в блок, щелчок по остальной площади описывающего прямоугольника блока (где зон нет) – не будет.