Навигация:
<< >> Оглавление Указатель
Текст С++

Руководство программиста

Глава 2. Создание моделей блоков

§2.11. Отображение всплывающих подсказок к блокам

Рассматривается вывод различных текстов во всплывающих подсказках к блокам. Приводятся примеры отображения в подсказке настроечных параметров блока-генератора и координат ближайшей к курсору точки графика.

Всплывающие подсказки – это окна с текстом, появляющиеся через некоторое время после наведения курсора на какой-либо элемент окна: кнопку, поле ввода и т.п. Они широко используются в Windows для вывода пояснений к различным объектам, поскольку текст, показываемый таким образом, появляется только при необходимости и не занимает места на экране в остальное время. RDS позволяет моделям выводить всплывающие подсказки к обслуживаемым этими моделями блокам, причем текст подсказки, время ее нахождения на экране и некоторые другие параметры могут задаваться программно. Если в параметрах блока разрешен вывод всплывающей подсказки (см. рис. 7), при задержке курсора над изображением этого блока RDS вызовет его модель в режиме RDS_BFM_POPUPHINT, передав ей указатель на структуру RDS_POPUPHINTDATA, содержащую различные параметры, связанные с выводом подсказки: координаты курсора мыши, текущий масштаб подсистемы, временной интервал гашения подсказки и т.п. Некоторые параметры в этой структуре модель может изменить, например, увеличить или уменьшить задержку гашения подсказки, или указать на необходимость погасить ее немедленно при перемещении курсора. Установив текст подсказки при помощи сервисной функции rdsSetHintText и, при необходимости, изменив параметры в переданной структуре, модель возвращает управление RDS, после чего подсказка появляется на экране. Если модель не установит никакого текста функцией , подсказка выведена не будет.

Следует помнить, что вызов устанавливает текст подсказки только внутри реакции модели на сообщение . Во всех остальных реакциях модели вызов будет проигнорирован. Таким образом, нельзя установить текст подсказки один раз, например, при создании блока. Этот текст должен передаваться в RDS каждый раз перед выводом подсказки. Поскольку всплывающая подсказка, как правило, должна зависеть от текущего состояния и режима работы блока, такая схема оправдана, так как текст подсказки может меняться от показа к показу.

В §2.7.2 рассмотрен блок, который, в зависимости от настроек, может выводить на выход синусоидальный и косинусоидальный сигнал или прямоугольные импульсы заданной скважности. Сделаем для этого блока всплывающую подсказку, которая будет отображать параметры блока, а также, при необходимости, сообщать об отсутствии доступа к переменной времени «DynTime», необходимой блоку для работы.

Добавим в класс личной области данных блока TTestGenData новую функцию PopupHint, которая будет вызываться из модели и формировать всплывающую подсказку.

  //====== Класс личной области данных ======
  class TTestGenData
  { public:
      int Type;       // Тип (0-sin,1-cos,2-прямоугольные)
      double Period;  // Период
      double Impulse; // Длительность импульса

       Time; // Связь с динамической
                            // переменной времени

      int Setup(void);           // Функция настройки
      void SaveText(void);       // Сохранение параметров
      void LoadText(char *text); // Загрузка параметров
      void PopupHint(void);      // Всплывающая подсказка  
      TTestGenData(void)         // Конструктор класса
        { Type=0; Period=1.0; Impulse=0.5;
          // Подписка на динамическую переменную времени
          Time=(,
                                        "DynTime",
                                        "D",
                                        TRUE);
        };
      ~TTestGenData(void)        // Деструктор класса
        { // Прекращение подписки
          (Time);
        };
  };
  //=================================================

В функцию модели блока необходимо добавить реакцию на вызов в режиме :

        // …
        // Очистка
        case :
          data=(TTestGenData*)(BlockData->BlockData);
          delete data;
          break;
        // Всплывающая подсказка                       
        case :                        
          data=(TTestGenData*)(BlockData->BlockData);  
          data->PopupHint();                           
          break;                                       
        // Проверка типа переменных
        case :
          if(strcmp((char*)ExtParam,"{SSD}")==0)
            return ;
          return ;
        // …

В данном случае нам не нужны параметры из структуры , поэтому в реакции на мы игнорируем указатель на структуру, переданный в ExtParam, и не передаем ничего в функцию PopupHint.

Функция вывода всплывающей подсказки будет выглядеть следующим образом:

  // Вывод всплывающей подсказки
  // 
  // 
  // 
  void TTestGenData::PopupHint(void)
  { // Есть ли доступ к переменной времени?
    if(Time==NULL || Time->Data==NULL) // Доступа нет
      ("ОШИБКА: в схеме нет переменной DynTime");
    else	// Доступ есть
      { char buf[200];	// В этом буфере будем формировать текст
        switch(Type)
          { case 0:	// Синус
              sprintf(buf,"Генератор - синус\nПериод: %lf",Period);
              break;
            case 1:	// Косинус
              sprintf(buf,"Генератор – косинус\n"
                          "Период: %lf",Period);
              break;
            case 2:	// Импульсы
              sprintf(buf,"Генератор – прямоугольные импульсы\n"
                          "Период: %lf\n"
                          "Длительность импульса: %lf",
                          Period,Impulse);
              break;
            default:	// Недопустимый тип
              return;
          }
        // Установка текста подсказки
        (buf);
      }
  }
  //=================================================

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

Если же доступ к динамической переменной есть, функция формирует текст подсказки с названием типа генерируемого сигнала и его параметрами. Текст записывается во вспомогательный массив buf стандартной библиотечной функцией форматированного вывода sprintf (для ее использования необходимо включить в исходный текст модели заголовочный файл «stdio.h»). Текст, кроме названия сигнала, содержит значение его периода и, если поле Type равно двум, еще и длительность прямоугольного импульса. Период и длительность импульса – вещественные числа двойной точности (тип double), поэтому для их вывода используется формат «%lf». Далее сформированный в buf текст передается в RDS для использования в качестве подсказки. Символы перевода строки «\n» в тексте служат для разбиения подсказки на строки.

Всплывающая подсказка к генератору

Рис. 71. Всплывающая подсказка к генератору

Теперь, если установить в параметрах блока на вкладке «DLL» флаг «блок выводит всплывающую подсказку», при наведении курсора мыши на блок через некоторое время будет появляться окно подсказки со сформированным текстом (рис. 71).

Использование формата «%lf» для вывода периода и длительности импульса в данном случае выглядит не очень удачным, поскольку в обоих числах выведено слишком много незначащих нулей. Явно указать число знаков после десятичной точки затруднительно, поскольку заранее неизвестен масштаб чисел, с которыми будет работать пользователь. Здесь было бы желательно автоматически подбирать формат вывода под введенные пользователем числа. К счастью, в RDS есть подобная сервисная функция, которой мы и воспользуемся для улучшения внешнего вида подсказки. Переделаем функцию PopupHint следующим образом:

  // Вывод всплывающей подсказки
  // 
  // 
  // 
  void TTestGenData::PopupHint(void)
  { // Есть ли доступ к переменной времени?
    if(Time==NULL || Time->Data==NULL) // Доступа нет
      ("ОШИБКА: в схеме нет переменной DynTime");
    else // Доступ есть
      { char *str=NULL,
             *period=(Period,-1,NULL),
             *impulse=(Impulse,-1,NULL);
        switch(Type)
          { case 0: // Синус
              str=("Генератор - синус\nПериод: ",
                               period,FALSE);
              break;
            case 1: // Косинус
              str=("Генератор – косинус\nПериод: ",
                               period,FALSE);
              break;
            case 2: // Импульсы
              str=("Генератор – прямоугольные импульсы\n"
                          "Период: ",period,FALSE);
              (&str,"\nДлительность импульса: ",FALSE);
              (&str,impulse,FALSE);
              break;
          }
        // Установка текста подсказки
        (str);
        // Освобождение памяти, занятой динамическими строками
        (str);
        (period);
        (impulse);
      }
  }
  //=================================================

Начало новой функции не отличается от прежнего варианта: при отсутствии доступа к переменной времени в качестве текста подсказки устанавливается сообщение об ошибке. Далее начинаются различия: если раньше мы формировали текст подсказки во вспомогательном массиве buf, теперь для этого будут использоваться сервисные функции RDS, которые сами отводят память под строки. Для хранения указателя на динамически отведенную строку с текстом подсказки будем использовать вспомогательную переменную str. Двум другим вспомогательным переменным, period и impulse, сразу же присваиваются указатели на динамические строки, в которые преобразуются вещественные значения Period и Impulse соответственно. Для преобразования используется сервисная функция RDS rdsDtoA, принимающая три параметра: преобразуемое вещественное число, желаемое число знаков после десятичной точки и указатель на целую переменную, в которую нужно записать длину получившейся строки. Вместо числа знаков после десятичной точки мы в обоих вызовах передаем число −1, указывающее функции на необходимость самостоятельно подобрать точность для преобразования числа и отбросить все незначащие нули в дробной части. Длина получившейся строки нас не интересует, поэтому в третьем параметре функции в обоих случаях передается NULL. Теперь у нас есть две динамические строки, содержащие символьное представление чисел Period и Impulse, которые будут использоваться при формировании текста подсказки. Важно не забыть освободить отведенную под них память, вызвав в конце функции rdsFree.

Далее, в зависимости от значения Type, мы формируем динамическую строку с подсказкой и записываем указатель на нее в переменную str. Эту строку тоже нужно будет освободить в конце функции при помощи . Для значений Type 0 и 1 процедура проста: мы используем уже знакомую нам функцию rdsDynStrCat, которая складывает две переданные ей строки. В данном случае первая строка – заголовок подсказки с названием формы сигнала и словом «период», вторая – значение периода. В третьем параметре функции передается FALSE, что запрещает возврат NULL вместо строки нулевой длины. На этом формирование подсказки для синусоидального и косинусоидального сигналов заканчивается.

Для прямоугольных импульсов (Type==2) подсказка формируется немного сложнее: кроме значения периода сигнала в подсказку нужно добавить значение ширины импульса. Сначала, как и для двух других форм сигнала, при помощи формируется строка, состоящая из заголовка подсказки и значения периода сигнала. После этого к уже сформированной динамической строке необходимо добавить слова «длительность импульса» и значение этой длительности. Для этого мы будем пользоваться не функцией , а очень похожей на нее rdsAddToDynStr. Отличие этой функции заключается в том, что она добавляет к уже имеющейся динамической строке, переданной в первом параметре, строку, переданную во втором, увеличивая отведенную для первой строки память. Фактически, вызову

  (&str,text,FALSE);

соответствует конструкция следующего вида:

  char *oldstr=str;
  str=(oldstr,text,FALSE);
  (oldstr);

При использовании этой функции следует всегда помнить, что в качестве первого параметра в нее передается указатель на переменную, содержащую указатель на динамическую строку. Эта строка не может быть статической, поскольку после формирования новой объединенной строки пытается освободить старую строку при помощи . Вызов не для динамической строки может привести к непредсказуемым последствиям. Например, конструкция

  const char teststr[]="Некоторая статическая строка";
  char *str=(char*)(&teststr);
  (&str,"добавляемая строка",FALSE);

является ошибочной, поскольку переменная str в данном случае указывает не на динамическую строку, а на массив teststr, и при попытке освободить занятую teststr память произойдет ошибка.

Вернемся к переделанной функции PopupHint при Type==2. Первый вызов добавит к уже сформированному в str тексту подсказки слова «длительность импульса» с двоеточием, второй – строку, в которую ранее было преобразовано значение этой длительности. Затем сформированный текст передается в RDS при помощи rdsSetHintText, после чего все три использованные в функции динамические строки освобождаются вызовами . Внешне подсказка, выведенная этой функцией, будет выглядеть так же, как и на рис. 71, только вместо «4.000000» и «2.000000» будут выведены числа «4» и «2» соответственно.

В описанном примере мы не использовали параметры всплывающей подсказки, передаваемые в модель блока при ее вызове в режиме , и не пытались изменить поведение этой подсказки на экране. Рассмотрим теперь более сложный случай: сделаем всплывающую подсказку к графику, рассмотренному в §2.10.1. Эта подсказка должна отображать координаты точки графика возле курсора мыши. Таким образом, при перемещении курсора над изображением графика текст всплывающей подсказки должен изменяться, отражая изменение координат точки под курсором. Без изменения некоторых параметров подсказки этого добиться не получится: по умолчанию она просто исчезнет через некоторое время после появления, не реагируя на перемещения курсора в пределах изображения блока.

В описание класса блока нам нужно добавить две новых функции: одна из них будет заниматься выводом всплывающей подсказки, другая, вспомогательная, будет искать в массиве индекс отсчета, ближайшего к переданному в параметре значению времени. Функция, выводящая всплывающую подсказку, будет принимать один параметр: указатель на структуру параметров подсказки RDS_POPUPHINTDATA:

  //=========================================
  // Простой график – личная область данных
  //=========================================
  class TSimplePlotData
  {
      // …

      // Рисование иконки при отсутствии доступа к DynTime
      void DrawAdditional( DrawData);
// Поиск индекса отсчета, соответствующего времени t int FindTimeIndex(double t); // Вывод всплывающей полсказки void PopupHint( hintdata);
// … // … далее без изменений …

Напишем сначала функцию поиска отсчета в массиве Times, учитывая тот факт, что из-за логики работы блока он упорядочен по возрастанию. Чтобы не усложнять пример, будем искать отсчет линейным поиском (позже мы перепишем эту функцию с использованием более быстрого алгоритма поиска).

  // Поиск отсчета, соответствующего времени t
  int TSimplePlotData::FindTimeIndex(double t)
  {
    if(Count==0) // Массивы пусты
      return -1;

    if(t<=Times[0]) // t раньше начала массива
      return 0; // Ближайший индекс - 0

    // Поиск первого индекса, большего t
    for(int i=1;i<NextIndex;i++)
      if(Times[i]>t) // t между i-1 и i
        { double d1=fabs(t-Times[i-1]),
                 d2=fabs(t-Times[i]);
          return (d1<d2)?(i-1):i; // Возвращаем ближайший
        }
    // Ничего не нашли – значит, t>Times[NextIndex-1]
    return NextIndex-1;
  }
  //=========================================

Эта функция устроена достаточно просто. Сначала мы проверяем, есть ли вообще отсчеты в массивах, и, если их нет, возвращаем вместо индекса значение −1. Затем сравниваем переданное значение времени t с началом массива. Если t меньше Times[0], значит, все отсчеты массива находятся позднее t, и ближайший к t индекс – это начало массива, то есть 0. Далее мы в цикле проходим по массиву и ищем в нем самый первый отсчет, больший t. Если такой найдется, значит, t лежит между ним и предыдущим. Остается только определить, к какому из этих двух отсчетов t ближе, и возвратить соответствующий индекс. Если же во всем массиве не нашлось ни одного отсчета, большего t, значит, ближайшим к t будет конец массива, то есть NextIndex-1. При большом количестве отсчетов функция будет работать медленно, но при выводе всплывающей подсказки особенной скорости обычно не требуется. Тем более, что позже мы переделаем эту функцию.

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

  // Всплывающая подсказка
  // 
  // 
  // 
  void TSimplePlotData::PopupHint( hintdata)
  { int x1,x2,y1,y2,index;
    double t;
    char *text,*str_t,*str_v;

    // Проверка доступа к переменной времени
    if(Time==NULL || Time->Data==NULL)
      { ("ОШИБКА: в схеме нет переменной DynTime");
        return;
      }

    // Определение абсолютных координат поля графика
    x1=hintdata->Left+Gr_x1;
    x2=hintdata->Left+Gr_x2;
    y1=hintdata->Top+Gr_y1;
    y2=hintdata->Top+Gr_y2;

    // Если курсор мыши не попадает в поле графика,
    // подсказку выводить не нужно
    if(hintdata->x<x1 || hintdata->x>x2 ||
       hintdata->y<y1 || hintdata->y>y2)
      return;

    // Преобразование экранной горизонтальной координаты
    // в значение времени согласно масштабу графика
    t=(hintdata->x-x1)*(Xmax-Xmin)/(x2-x1)+Xmin;

    // Поиск отсчета, соответствующего этому моменту времени
    index=FindTimeIndex(t);
    if(index<0) return; // Ошибка – отчет не найден

    // Преобразование времени и значения отсчета в строки
    str_t=(Times[index],-1,NULL);
    str_v=(Values[index],-1,NULL);
    // Формирование текста подсказки
    text=("Время: ",str_t,FALSE);
    (&text,"\nЗначение: ",FALSE);
    (&text,str_v,FALSE);
    // Установка текста подсказки
    (text);
    // Освобождение динамических строк
    (text);
    (str_t);
    (str_v);

    // Изменение параметров подсказки таким образом, чтобы при
    // смещении курсора на одну точку она вывелась снова
    hintdata->HZLeft=hintdata->x;
    hintdata->HZTop=hintdata->y;
    hintdata->HZWidth=hintdata->HZHeight=1;
    // Задержка гашения подсказки – одна минута
    hintdata->HideTimeout=60000;
  }
  //=========================================

Эта функция принимает единственный параметр – указатель на структуру параметров подсказки RDS_POPUPHINTDATA. Этот указатель RDS передает в модель блока, а она, в свою очередь, должна будет передать ее нашей новой функции. Структура описана следующим образом:

  typedef struct
  { int x,y;              // Координаты курсора
    int BlockX,BlockY;    // Точка привязки блока
    // Положение и размер блока на момент последнего рисования
    int Left,Top;         // Левый верхний угол
    int Width,Height;     // Ширина и высота
    //------- Параметры, которые можно изменить -------
    int HZLeft,HZTop,     // Размер зоны действия подсказки (по
        HZWidth,HZHeight; // умолчанию – все изображение блока)
    int ReshowTimeout;    // Интервал повторного вывода подсказки
                          // (по умолчанию – 0, то есть не выводить)
    int HideTimeout;      // Интервал гашения подсказки (по
                          // умолчанию – стандартное для Windows)
    //---------- Конец изменяемых параметров ----------
    int IntZoom;          // Масштаб окна в %
    double DoubleZoom;    // Масштабный к-т (в долях единицы)
  } ;
  typedef  * ;

В этой структуре нас, прежде всего, интересуют координаты курсора мыши (x,y) и верхнего левого угла блока (Left,Top). Зная их, мы можем вычислить, какой точке массива соответствует положение курсора. Кроме того, мы будем менять зону действия подсказки (HZLeft, HZTop, HZWidth и HZHeight). Пока курсор не покинет эту зону, подсказка меняться не будет. Нам нужно, чтобы текст подсказки отражал координаты точки графика, около которой находится курсор, поэтому мы каждый раз будем устанавливать зону действия размером в одну точку точно под курсором. Таким образом, любое перемещение курсора будет приводить к его выходу из зоны действия подсказки и выводу новой, с новым текстом. Мы также увеличим время гашения подсказки (HideTimeout) до одной минуты, чтобы пользователь гарантированно успел прочесть ее. При выходе курсора за пределы блока подсказка будет погашена независимо от значения HideTimeout, поэтому мы можем не беспокоиться о том, что этот интервал слишком велик.

Как и в предыдущем примере с подсказкой, сначала мы проверим доступность динамической переменной времени и, в случае ее отсутствия, выведем вместо подсказки сообщение об ошибке. В совокупности с изображаемой блоком иконкой с восклицательным знаком (см. §2.10.3), это сообщение позволит пользователю понять причину неработоспособности блока. Если же с переменной времени все в порядке, то, как и в функции рисования, мы вычисляем абсолютные координаты поля графика в окне подсистемы. В функции рисования мы пользовались для этого полями Left и Top структуры RDS_DRAWDATA, здесь пользуемся одноименными полями – их значения будут одинаковыми. Если курсор мыши, координаты которого также передаются в структуре , не попадает в поле графика, функция немедленно завершается и подсказка при этом не выводится. Если же курсор находится внутри поля, мы, согласно масштабу горизонтальной оси, вычисляем значение времени, которому соответствует горизонтальная координата курсора, и записываем его в переменную t. Используемая при этом формула обратна формуле преобразования времени в координату, использованной в функции рисования.

Далее, при помощи написанной ранее функции FindTimeIndex, мы вычисляем индекс в массиве отсчетов, которому соответствует значение времени t и записываем его в целую переменную index. Возврат функцией отрицательного значения свидетельствует об ошибке (массивы пусты), в этом случае функция завершается без вывода подсказки. Теперь, зная индекс в массивах, можно определить значение времени Times[index] и вертикальной координаты Values[index], соответствующие текущей точке, и сформировать из них текст подсказки уже описывавшимся в предыдущем примере способом.

Теперь нужно вставить вызов PopupHint в функцию модели блока – для этого в оператор switch(CallMode) добавляется новая метка case:

      // …

// Всплывающая подсказка case : data->PopupHint(()ExtParam); break;
// …

Здесь перед передачей в функцию PopupHint указатель общего вида (void*) ExtParam, полученный функцией модели блока, приводится к типу , то есть «указатель на RDS_POPUPHINTDATA».

Всплывающая подсказка к графику

Рис. 72. Всплывающая подсказка к графику

Осталось только настроить зону действия подсказки и время ее гашения. В координаты левого верхнего угла зоны копируются координаты курсора мыши, а размер зоны устанавливается в одну точку – теперь любое перемещение курсора выведет его за пределы зоны, и подсказка будет показана повторно, уже с новыми значениями координат. Время гашения подсказки устанавливается в 60000 миллисекунд, то есть в одну минуту. Теперь, если разрешить вывод всплывающей подсказки для блока, и навести курсор на поле графика, через некоторое время появится окно подсказки (рис. 72), которое будет перемещаться вслед за курсором мыши, и числа в нем будут изменяться).

Написанная нами функция FindTimeIndex определяет соответствующий заданному времени отсчет в массиве при помощи простого линейного поиска. Перепишем ее так, чтобы она использовала более быстрый поиск делением пополам, пользуясь тем, что массив Times упорядочен по возрастанию. Хотя используемый метод поиска и не имеет прямого отношения к рассматриваемому в примерах выводу всплывающих подсказок, следует помнить, что линейный поиск – достаточно медленный алгоритм, и при больших массивах отсчетов он может вызывать замедление работы при перемещениях курсора по графику. Поэтому лучше ускорить указанную функцию, тем более, что метод деления пополам не особенно сложен. Новая функция FindTimeIndex будет иметь следующий вид:

  // Поиск отсчета, соответствующего времени t
  // (метод деления пополам)
  int TSimplePlotData::FindTimeIndex(double t)
  { int L,R;
    double dl,dr;

    if(Count==0 || NextIndex==0) // Нет данных в массивах
      return -1;
    if(NextIndex==1) // В массиве единственный отсчет
      return 0;

    // В массиве по крайней мере два значения – проверяем границы
    if(t<=Times[0]) // t меньше первого отсчета
      return 0;
    if(t>=Times[NextIndex-1]) // t больше последнего отсчета
      return NextIndex-1;

    // t - внутри диапазона массива
    L=0; R=NextIndex-1;
    while(L<R-1)
      { int m=(L+R)/2;
        if(Times[m]<t) L=m; else R=m;
      }
    // t лежит между L и R
    dl=fabs(t-Times[L]),
    dr=fabs(t-Times[R]);
    return (dl<dr)?L:R; // Возвращаем ближайший
  }
  //=========================================

Проверив, что в массиве есть данные, и исключив случай, когда в нем записан единственный отсчет (при этом ничего искать, естественно, не нужно), мы сравниваем значение t с первым и последним отсчетами массива. Если t лежит за пределами диапазона отсчетов массива, искать его в массиве не нужно: ближайшим к t отсчетом будет первый (если t<=Times[0]) или последний (если t>=Times[NextIndex-1]). Если t – внутри диапазона, мы присваиваем переменным L и R граничные индексы массива и начинаем в цикле сравнивать отсчет в середине диапазона LR с t. В зависимости от того, будет этот отсчет меньше или больше t, мы перемещаем одну из границ и повторяем деление до тех пор, пока L и R не окажутся соседними отсчетами, при этом значение t будет лежать между ними. Останется только выбрать из этих отсчетов ближайший к t.


<< >> Оглавление Указатель