1.3. Меню, вкладки форм, кнопки, полосы прокрутки, «горячие клавиши» обработка сообщений ON_WM_SIZE и шаблонная обработка команд при работе с потомками классов CListCtrl на диалогах типа CFormView

Этот материал ранее был опубликован в виде статьи на http://www.codeproject.com (на английском языке) после обсуждения на форуме http://sql.ru/forum/C++/.

Здесь фактически воспроизводится итоговый материал обсуждения из того же форума:

Меню, вкладки форм, кнопки, полосы прокрутки, «горячие клавиши» обработка сообщений ON_WM_SIZE и шаблонная обработка команд при работе с потомками классов CListCtrl на диалогах типа CFormView

Июль 2009 года

Продемонстрирована возможность использования стандартного интерфейса MDI приложения для управления модифицированными списками MFC.

Загрузить демонстрационный файл с www.codeproject.com (требуется регистрация (бесплатная) на этом сайте):

ModifiedListCtrl03.zip - 77.3 Kb

Загрузить демонстрационный файл с sql.ru (сначала просто зайдите на этот сайт, затем воспользуйтесь ссылкой):

ModifiedListCtrl03.zip - 77.3 Kb

Загрузить тот же файл отсюда (переименуйте расширение в zip):

ModifiedListCtrl.003 - 74.0 Kb

Введение

Тема стандартного MFC интерфейса не является ни новой, ни трудной. Однако, так или иначе ей приходиться уделять должное внимание, во-первых потому, что без нее нельзя двигаться дальше в программировании, а во-вторых, в связи с тем, что для реального приложения, которые мы планируем осуществить на базе данных примеров, все же требуется некоторая адаптация к нашему проекту имеющихся наработок в этой области. Ибо обычно демонстрируемые примеры программируются абстрактно и вообще, как правило, оторвано от рабочих приложений. Наши демонстрации мы будем делать «конкретными», и в том объеме, в котором это реально необходимо, имеющем конечной целью получить практическое приложение.

Продолжаем развивать наш предыдущий проект. Добавим теперь некоторые стандартные элементы интерфейса для управления нашими диалоговыми формами с классами порожденными от CListCtrl. Каждый из этих элементов не представляет особых трудностей, однако некоторые нюансы все же имеются. В результате получим пример указанный на рис. 1

Модифицированные списки на измененных формах с элементами пользовательского интерфейса
Рис. 1. Модифицированные списки на измененных формах с элементами пользовательского интерфейса.

1. Scrollbars

Когда формы списков имеют достаточно малый размер, на них появляются горизонтальные и вертикальные скрулбары (полосы прокрутки). Это свойство по умолчанию нам совершенно не нужно и мы принудительно отключаем их. Для этого мы используем следующий код:

/////////////////////////////////////////////////////////////////////////
// OnInitialUpdate
/////////////////////////////////////////////////////////////////////////
void CMainView::OnInitialUpdate() {
  CFormView::OnInitialUpdate();

  //*** Don't set the Size to {0} else the debug version of the program 
  // will be wrong!
  //SIZE Size = {1, 1};

  //*** Turns off scroll bars of the Form
  //SetScaleToFitSize(Size);
  
  SIZE Size = {0};

  //*** Turns off scroll bars of the Form
  SetScrollSizes(MM_TEXT, Size);
}  // OnInitialUpdate

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

/////////////////////////////////////////////////////////////////////////
// OnCreate
/////////////////////////////////////////////////////////////////////////
int CMainView::OnCreate(LPCREATESTRUCT pCS) {
  . . .

  //*** Shows the vertical scroll bar always
  pTable->ShowScrollBar(SB_VERT);

  . . .
}  // OnCreate

и аналогичный код в обработчике:

/////////////////////////////////////////////////////////////////////////
// OnSize
/////////////////////////////////////////////////////////////////////////
void CChildFrame::OnSize(UINT nType, int cx, int cy) {
  . . .

  //*** Shows the vertical scroll bar always
  m_pTable->ShowScrollBar(SB_VERT);
}  // OnSize

2. Обработка ON_WM_SIZE сообщений для главного и дочерних фрэймов

Если мы изменяем размер дочернего фрэйма, то таблица внутри него становиться недоступной для использования, так как ее полосы прокрутки становятся невидимыми. Аналогичная ситуация, когда мы меняем размер главного, родительского фрэйма. Дочерние окна в нем могут также иметь невидимые полосы прокрутки. Эта ситуация наблюдается в русской учетной программе «1С77». При уменьшении ее главного окна, дочерние окна могут стать недоступными для управления с помощью скрулбаров. Чтобы избавиться от такой ситуации мы будем обрабатывать сообщение ON_WM_SIZE для родительского и дочернего фрэймов.

В дочернем фрэйме имеем следующий код:

/////////////////////////////////////////////////////////////////////////
// OnSize
/////////////////////////////////////////////////////////////////////////
void CChildFrame::OnSize(UINT nType, int cx, int cy) {
  CMDIChildWnd::OnSize(nType, cx, cy);

  //*** Common offset for child frames
  int m_nChildFrmOffset = m_pMainApp->m_nChildFrmOffset;

  //*** Common width of main frame for border
  UINT m_nMainFrmBorders = m_pMainApp->m_nMainFrmBorders;

  //*** Current table pointer
  m_pTable = m_pMainApp->m_apTable[m_eTable];

  if(!m_pTable) {
    _M("CChildFrame: Empty a CListCtrlEx object!");
    return;
  }
  
  //*** Offset & Border size
  cx = cx - m_nMainFrmBorders + m_nChildFrmOffset;
  cy = cy - m_nMainFrmBorders + m_nChildFrmOffset;

  //*** Uses suitable sizes
  RECT Rect = {m_nChildFrmOffset, m_nChildFrmOffset, cx, cy};
  
  //*** Changes a window sizes
  m_pTable->MoveWindow(&Rect);

  //*** Shows the vertical scroll bar always
  m_pTable->ShowScrollBar(SB_VERT);
}  // OnSize

А в родительском фрейме нам нужно будет обработать уже не одно дочернее окно, а все их множество. Это достигается следующим кодом:

/////////////////////////////////////////////////////////////////////////
// OnSize
/////////////////////////////////////////////////////////////////////////
void CMainFrame::OnSize(UINT nType, int cx, int cy) {
  CMDIFrameWnd::OnSize(nType, cx, cy);

  //*** True sizes of the main frame client
  int nMainWidth = cx - m_pMainApp->m_nMainFrmBorders;
  int nMainHeight = cy - m_pMainApp->m_nClientCtrlsHeight;

  //*** Current child frame rectangle
  RECT ChildRect = {0};  // For safe change a child rectangle
  RECT *pChildRect = NULL;

  //*** Table Id
  ETABLE eTable = m_pMainApp->m_eTable;

  if(!eTable) {
    //_M("CMainFrame: No objects is in the application!");
    return;  // Simply exit
  }

  //*** Current child frame pointer
  CChildFrame **apChildFrame = m_pMainApp->m_apFrame;
  CChildFrame *pChildFrame = NULL;

  //*** The meta table structure
  META_TABLE *aMetaTable = m_pMainApp->m_aMetaTable;

  //*** Sizes of child frames
  int nChildLeft = 0;
  int nChildTop = 0;
  int nChildWidth = 0;
  int nChildHeight = 0;

  //*** Look at the child frame array
  for(UINT i = e_NULL; i < e_MAX; i++) {
    pChildFrame = apChildFrame[i];

    if(!pChildFrame)
        continue;
    
    pChildRect = aMetaTable[i].pFrmRect;

    if(!pChildRect) {
      _M("CChildFrame: Empty a child rectangle!");
      return;
    }

    //*** Sizes of the child frame
    nChildLeft = pChildRect->left;
    nChildTop = pChildRect->top;
    nChildWidth = pChildRect->right - pChildRect->left;
    nChildHeight = pChildRect->bottom - pChildRect->top;

    //*** Changes sizes and locality of the child frame

    if(nChildWidth > nMainWidth) {
      ChildRect.left = 0;
      ChildRect.right = nMainWidth;
    } else if(nChildLeft > nMainWidth - nChildWidth) {
      ChildRect.left = nMainWidth - nChildWidth;
      ChildRect.right = nMainWidth;
    } else {
      ChildRect.left = nChildLeft;
      ChildRect.right = nChildLeft + nChildWidth;
    }

    if(nChildHeight > nMainHeight) {
      ChildRect.top = 0;
      ChildRect.bottom = nMainHeight;
    } else if(nChildTop > nMainHeight - nChildHeight) {
      ChildRect.top = nMainHeight - nChildHeight;
      ChildRect.bottom = nMainHeight;
    } else {
      ChildRect.top = nChildTop;
      ChildRect.bottom = nChildTop + nChildHeight;
    }

    pChildFrame->MoveWindow(&ChildRect);
  }

  pChildFrame = m_pMainApp->m_apFrame[eTable];

  if(pChildFrame) {
    //*** Activates current child frame
    pChildFrame->ActivateFrame();

    //*** Updates tabs
    m_MainTabs.Update();
  }
}  // OnSize

3. Табы на главном фрэйме

Для организации табов на главном фрэйме мы воспользовались классами CMDITabs от Christian Rodemeyer . Их оформление в нашей программе можно видеть на рис. 1.

4. Меню

Построение пользовательского субменю не представляет никаких проблем. Единственный нюанс, это автоматическое создание элементов субменю Window, которые соответствуют открытым формам приложения. Поскольку обработчики для этих команд меню пишем не мы и доступа к ним не имеем, то при их выполнении не происходит обновление табов с помощью функции m_MainTabs.Update. Чтобы каким-то образом перехватить эти системные обработчики мы включили в нашу карту сообщений обработку сообщения ON_WM_CHILDACTIVATE. Вот этот обработчик:

/////////////////////////////////////////////////////////////////////////
// OnChildActivate
/////////////////////////////////////////////////////////////////////////
void CChildFrame::OnChildActivate() {
  CMDIChildWnd::OnChildActivate();

  //*** Saves current table Id in the main application
  m_pMainApp->m_eTable = m_eTable;

  //*** Updates tabs
  m_pMainFrame->m_MainTabs.Update();
}  // OnChildActivate

В результате получаем необходимое обновление табов на главном фрэйме.

5. Тулбары (прилегающие друг к другу)

При создании нашего тулбара он располагается по умолчанию с «новой строки» по отношению к созданному ранее тулбару. Чтобы пристыковать «новый» контрол к «старому» в одну строку (side by side) можно воспользоваться третьим параметром в функции главного фрэйма:

void DockControlBar(CControlBar *pBar, UINT nDockBarID = 0,
    LPCRECT lpRect = NULL);

Однако, этот метод не работает до тех пор пока мы не воспользуемся советом от Kirk Stowell который предлагает вызвать функцию RecalcLayout перед функцией DockControlBar. В итоге имеем:

/////////////////////////////////////////////////////////////////////////
// OnCreate
/////////////////////////////////////////////////////////////////////////
int CMainFrame::OnCreate(LPCREATESTRUCT pCS) {
  . . .

  //*** To enable dockable control bars in the frame window
  EnableDocking(CBRS_ALIGN_ANY);
  
  //*** To enable the control bars to be docked
  m_ToolBar.EnableDocking(CBRS_ALIGN_ANY);
  m_ToolBar2.EnableDocking(CBRS_ALIGN_ANY);
  
  //*** Causes the control bar to be docked to the frame window
  DockControlBar(&m_ToolBar);

  //*** Called when the control bars are toggled on or off or when the
  // frame window is resized
  RecalcLayout();
  
  RECT Rect = {0};
  
  //*** Copies the dimensions of the bounding rectangle of the toolbar to 
  // the structure pointed to by &Rect
  m_ToolBar.GetWindowRect(&Rect);

  //*** Docking the toolbars side by side

  Rect.left++;

  DockControlBar(&m_ToolBar2, AFX_IDW_DOCKBAR_TOP, &Rect);
  
  . . .
}  // OnCreate

6. «Горячие» клавиши

Для вызова наших таблиц назначены следующие клавиши:

F10 – First table;

F11 – Second table;

F12 – Third table.

7. Шаблонная обработка табличных команд

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

ON_COMMAND_RANGE(ID_TABLE_NULL, ID_TABLE_MAX, OnTable)

где функция OnTable определяется как:

/////////////////////////////////////////////////////////////////////////
// OnTable
/////////////////////////////////////////////////////////////////////////
void CMainApp::OnTable(UINT nTable) {
  //*** Calculates m_eTable
  m_eTable = (ETABLE) (nTable - ID_TABLE_NULL);

  if(m_eTable <= e_NULL || m_eTable >= e_MAX) {
    _M("No data is for this table!");
    return;
  }

  //*** Current child frame pointer
  CChildFrame *pChildFrame = m_apFrame[m_eTable];

  if(pChildFrame) {
    //*** Activates current child frame
    pChildFrame->ActivateFrame();

    //*** Checks the pointer before to call m_MainTabs.Update()
    if(!m_pMainFrame) {
      _M("CMainApp: Empty a CMainFrame object!");
      return;
    }

    //*** Updates tabs
    m_pMainFrame->m_MainTabs.Update();
    return;
  }

  //*** Creates a new child frame with a document
  //*
  CWinApp::OnFileNew();
  //*
  //*** Creates a new child frame with a document

  //*** Creates a new child frame without a document
  //*** Doesn't call CMainView::OnInitialUpdate() and ignores a call of
  //* CMainView::SetScaleToFitSize function 
  //*
  /*if(!m_pDocTemplate) {
    _M("CMainApp: Empty a CMultiDocTemplate object!");
    return;
  }
  
  pChildFrame = reinterpret_cast<CChildFrame *>(
      m_pDocTemplate->CreateNewFrame(NULL, NULL)
  );

  //*** Current meta table static structure
  META_TABLE MetaTable = m_aMetaTable[m_eTable];

  //*** Sets a title of child frame
  pChildFrame->SetTitle(MetaTable.szTblName);

  //*** Activates current child frame
  pChildFrame->ActivateFrame();*/
  //*
  //*** Creates a new child frame without a document

  //*** Checks the pointer before to call m_MainTabs.Update()
  if(!m_pMainFrame) {
    _M("CMainApp: Empty a CMainFrame object!");
    return;
  }

  //*** Updates tabs
  m_pMainFrame->m_MainTabs.Update();
}  // OnTable

Заметим, что если исключить функцию CWinApp::OnFileNew, то тогда надо убрать комментарий в коде, который создает новый дочерний фрэйм без сопутствующего документа. При этом функцию AddDocTemplate в CMainApp::InitInstance можно также исключить. Этот код оставлен для тех программистов которые не желают без крайней необходимости работать с классами документов. Единственные нюансы замеченные при этом состоят в том, что тогда не вызывается функция CMainView::OnInitialUpdate и игнорируется вызов функции CMainView::SetScaleToFitSize.




Сайт создан в системе uCoz