КАК добавить меню?

Сейчас я научу вас добавлять в вашу программу меню. Да, именно то самое меню, без которого не обходится ни одна программа Windows! Теперь меню будет и в вашей программе!
Меню бывают двух видов - главное меню и всплывающее. Первое размещается в окне под строкой заголовка, второе при правом щелчке мышью на каком-то объекте - рабочем поле, картинке и.т.д.
Рассмотрим сначала главное меню.


Главное меню


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


КАК создать простое меню?

Создайте новый проект, который назовите MyMenu. В пустой проект добавьте файл с текстом первой нашей программы, который назовите MyMenu.cpp. С исходным текстом пока всё понятно. Создайте пустой файл ресурсов и назовите его MyMenu.rc. Включите его в проект. Сейчас в него мы запишем код простого меню.

//Текст файла MyMenu.rc
#include "windows.h"

#define IDM_EXIT

MainMenu MENU //имя нашего меню MainMenu
{
POPUP "Файл" //Всплывающее меню "Файл"
{

MENUITEM "Выход", IDM_EXIT //Пункт меню. Ему соответствует идентификатор IDM_EXIT

}
}

Когда вы сохраните файл RC, то увидите, что во вкладке Resource вашего проекта, появится меню с именем MAINMENU.

Прекрасно. Теперь объявим имя этого меню в тексте программы, как мы это делали для иконок, курсоров и картинок:

char szMenu[]="MainMenu"; //разместите этй строку в начале программы

...
w.lpszMenuName=szMenu; //теперь поменяем ещё одну строчку.
// Наконец-то в структуре WNDCLASS появится ссылка на настоящее меню!


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

Поскольку меню, это своего рода, дочернее окно, ему нужен свой цикл обработки сообщений, чтобы задавать реакцию на выбор соответствующего пункта. Например, когда пользователь выберет пункт "Выход", программа должна завершить свою работу. Когда нажмёт "О программе...." - выдать небольшое диалоговое окно. Всё это прописывается в этом цикле.

КАК применить меню на практике?

Поразмышляем. Каждый пункт главного меню (на языке ресурсов POPUP) содержит пункты MENUITEM, которые содержат текст меню. Каждому пункту соответствует идентификатор, вроде IDM_EXIT. Идентификатор обычно начинается с префикса IDM_ (Identifer menu). В библиотечном файле содержатся идентификаторы пунктов меню.
Если в меню нужно вставить разделитель, которым обычно отделяют пункт "Выход" от остальных в меню "Файл", пишут так:

MENUITEM SEPARATOR //разделитель

Разделитель объявляется без идентификатора.

Если вы хотите, чтобы длинное меню состояло из двух колонок, начиная с этого пункта, добавляется слово MENUBARBREAK:

MENUITEM "&Disk space", IDM_FREE, MENUBARBREAK //начиная с этого
//пункта, меню будет идти в соседнем столбике.





Если надо сделать подменю, применима следующая конструкция:


POPUP "&User Name"
{
MENUITEM "User &number", IDM_NUM

POPUP "User &status"
{
MENUITEM "A&dministrator", IDM_ADMIN
MENUITEM "&Guest", IDM_GUEST
}

}

Таким образом, вкладка "User status" будет содержать подменю из двух пунктов: Administrator и Guest.


И ещё. Вы, наверное, обратили внимание на знак &, который я старательно вставляю между слов. Смысл его такой. Чтобы дать доступ пользователю к пунктам меню с клавиатуры, программист пишет знак амперсанд & перед той буквой, нажав которую, пользователь может выбрать этот пункт. Тогда в меню слово "Файл" (&Файл) , будет записано, как Файл, и если я нажму Alt+Ф, то перейду на этот пункт. Сейчас уже трудно представить себе пользователя, работающего в Windows XP без мышки.
Но признаком хорошего стиля программирования служит введение во все пункты возможности быстрого доступа с клавиатуры. Один из постулатов программирования: "Не надо решать за пользователя!". Да и меню без знаков подчёркивания выглядит каким-то голым.

Отдельно будет сказано о картинках в меню (это не так уже часто и нужно), а также о так называемых акселлераторах клавиатуры - специальной таблицы условленных сочетаний клавиш (Ctrl+O, Ctrl+N, Shift+Ins), нажав которые, пользователь тем самым вызовет обработку нажатия того пункта меню, которому соответствует это сочетание. Например пункт "Правка->Выделить всё" имеет клавиатурный эквивалент Ctrl+A. Согласитесь, что гораздо удобнее набирая текст нажать Ctrl+S для быстрого сохранения, чем браться за мышку и лезть в меню.

Теперь вы знаете всё, что нужно о синтаксисе меню в файле ресурсов. Для быстрого создания меню существует редактор ресурсов Visual C++. Добавив новый ресурс с помощью Ctrl+R, вы даёте ему имя. Во вкладке MENU появляется новое меню. Стоит щёлкнуть по нему два раза, и на экране появляется его схема, в которую вы можете вписывать новые пункты, добавлять разделители и вкладки, не прикладывая для этого никаких усилий.


Все свойства меню и его пунктов задаются через окно Properties.


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

switch(messg)
{

case WM_COMMAND:

switch(LOWORD(wParam)) {

//Выбор пункта с идентификатором IDM_ADMIN
case IDM_ADMIN: {
....
break;
}

//Выбор пункта, расположенного ниже
case IDM_GUEST: {
....
break;
}

}


....
}

Как видите, всё просто. Перехватываем значение wParam при поступлении WM_COMMAND. Раскладываем по полочкам наше меню и выполняем соответствующие процедуры.
Далее я привожу полный текст программы с использованием меню.

КАК использовать всю мощь меню?

Сейчас мы слегка дополним наш файл ресурсов, после чего в маленьком меню заметно прибавится пунктов. Конечная цель - создать что-то вроде системной утилиты, которая узнаёт сколько всего места у нас на диске, сколько на нём осталось свободного пространства и как называется текущая папка. В другом пункте меню можно будет узнать текущую дату и время, а в третьем - имя пользователя и имя компьютера. Все эти знания нам очень пригодятся.

//Текст файла MyMenu.h
//Каждому пункту по личному номеру!
#define IDM_EXIT 1
#define IDM_DSPACE 2
#define IDM_FREE 3
#define IDM_CURRENT 4
#define IDM_ABOUT 5
#define IDM_TIME 6
#define IDM_DATE 7
#define IDM_ACOMP 8

//Текст файла MyMenu.rc
#include "MyMenu.h"
#include "windows.h"

MainMenu MENU
{
//1-й пункт меню
POPUP "&Файл"
{
MENUITEM "В&ыход", IDM_EXIT
}

//2-й пункт меню
POPUP "&Диск"
{
MENUITEM "Всего на диске", IDM_DSPACE
MENUITEM "Свободное место", IDM_FREE
MENUITEM "Текущая папка", IDM_CURRENT
}

//3-й пункт меню
POPUP "Дата и время"
{
MENUITEM "Дата", IDM_DATE
MENUITEM "Время", IDM_TIME

}


//4-й пункт меню
POPUP "О компьютере"
{
MENUITEM "Имя пользователя", IDM_ABOUT
MENUITEM "Имя компьютера", IDM_ACOMP


}

//5-й пункт меню
POPUP "&Помощь"
{
MENUITEM "О программе", IDM_ABOUT
}

}


//MyMenu.cpp
#include <windows.h>
#include <stdio.h>
#include <time.h>

#include "MyMenu.h"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

char szProgName[]="Имя программы";
char str[]="Мой текст!";
char szMenu[]="MainMenu";
char szMessage[]="Выберите нужный пункт меню.";
short sLen;

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpszCmdLine, int nCmdShow)
{
HWND hWnd;
MSG lpMsg;


WNDCLASS w;
w.lpszClassName=szProgName; //имя программы
w.hInstance=hInstance;
w.lpfnWndProc=WndProc; //указатель на функцию окна
w.hCursor=LoadCursor(NULL, IDC_ARROW);
w.hIcon=LoadIcon(NULL, IDI_APPLICATION);
w.lpszMenuName=szMenu; //имя нашего меню
w.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH); //цвет фона окна
w.style=CS_HREDRAW|CS_VREDRAW; //стиль - перерисовываемое по х и по у
w.cbClsExtra=0;
w.cbWndExtra=0;

//Если не удалось зарегистрировать класс окна - выходим
if(!RegisterClass(&w))
return 0;

//Создадим окно в памяти, заполнив аргументы CreateWindow
hWnd=CreateWindow(szProgName,
"Демонстрация меню в Win32 API",
WS_OVERLAPPEDWINDOW,
100,
100,
500,
400,
(HWND)NULL,
(HMENU)NULL,
hInstance,
(LPSTR)NULL);

//Выводим окно из памяти на экран
ShowWindow(hWnd, nCmdShow);
//Обновим содержимое окна
UpdateWindow(hWnd);

while(GetMessage(&lpMsg, NULL, 0, 0)) {
TranslateMessage(&lpMsg);
DispatchMessage(&lpMsg);
}
return(lpMsg.wParam);
}

//Функция окна
LRESULT CALLBACK WndProc(HWND hWnd, UINT messg,
WPARAM wParam, LPARAM lParam)
{
HDC hdc; //создаём контекст устройства
PAINTSTRUCT ps; //создаём экземпляр структуры графического вывода

//дисковая информация
unsigned long lSectInClust, lByteInSect, lNumFreeClust, lNumClust, lTotal, lBuf, lBuf1;
char szCurrent[MAX_PATH];

//информация о дате и времени
SYSTEMTIME st;
WORD month, day, year, hour, min, sec;

//идентификация имени пользователя и компьютера
char ComputerName[MAX_COMPUTERNAME_LENGTH + 1];
unsigned long len_ComputerName = MAX_COMPUTERNAME_LENGTH + 1;
char UserName[256 + 1];
unsigned long len_UserName = 256 + 1;

BOOL comp;

//Цикл обработки сообщений
switch(messg)
{

case WM_COMMAND:
switch(LOWORD(wParam)){

//Выход из программы
case IDM_EXIT: DestroyWindow(hWnd);

//Сколько места на диске?
case IDM_DSPACE:
GetDiskFreeSpace(NULL, &lSectInClust, &lByteInSect, &lNumFreeClust, &lTotal);
lBuf1=lSectInClust*lByteInSect*lTotal;
sLen=sprintf(szMessage, "Всего на диске: %ld Мб", lBuf1/1000000);
break;

//Свободно на диске
case IDM_FREE:
GetDiskFreeSpace(NULL, &lSectInClust, &lByteInSect, &lNumFreeClust, &lTotal);
lBuf=lSectInClust;
lBuf*=lByteInSect;
lBuf*=lNumFreeClust;
sLen=sprintf(szMessage, "На диске свободно: %ld Мб", lBuf/1000000);
break;

//Текущая папка
case IDM_CURRENT:
GetCurrentDirectory(MAX_PATH, szCurrent);
sLen=sprintf(szMessage, "Тек. папка %s", szCurrent);
break;

//Текущее время
case IDM_TIME:
GetSystemTime(&st);

min=st.wMinute;
sec=st.wSecond;
hour=st.wHour;
if(min<10)
sLen=sprintf(szMessage, "Время: %d:0%d:%d", hour, min, sec);
else
sLen=sprintf(szMessage, "Время: %ld:%d:%d", hour, min, sec);
break;

//Текущая дата
case IDM_DATE:
GetSystemTime(&st);
year=st.wYear;
month=st.wMonth;
day=st.wDay;
if(month<10)
sLen=sprintf(szMessage, "Дата: %d/0%d/%d", day, month, year);
else
sLen=sprintf(szMessage, "Дата: %d/%d/%d", day, month, year);
break;

//Имя компьютера
case IDM_ACOMP:

comp = GetComputerName(ComputerName, &len_ComputerName);

if( comp != 0 )
{
sLen=sprintf(szMessage, "%s", ComputerName);
}
else
MessageBox(hWnd, "Имя компьютера не доступно!", "Сообщение:", MB_OK|MB_ICONSTOP);

break;

//Имя пользователя
case IDM_ABOUT:

comp = GetUserNameA (UserName, &len_UserName);

if( comp != 0 )
{
sLen=sprintf(szMessage, "%s", UserName);
}
else
MessageBox(hWnd, "Имя пользователя не доступно!", "Сообщение...", MB_OK|MB_ICONSTOP);

break;

}
InvalidateRect(hWnd, NULL, TRUE);
break;

//Вывод строки на экран
case WM_PAINT:
hdc=BeginPaint(hWnd, &ps);
TextOut(hdc, 10,10, szMessage, sLen);
ValidateRect(hWnd, NULL);
EndPaint(hWnd, &ps);
break;

//Завершение работы окна
case WM_DESTROY:
PostQuitMessage(0);
break;

default:
return(DefWindowProc(hWnd, messg, wParam, lParam));
}
return 0;
}

Программа работает так. Строка текста szMessage, длиной sLen выводится на экран только при поступлении сообщения WM_PAINT. Каждый раз выделять контекст при выборе пункта меню мы бы замучились. При выборе каждого пункта, мы просто заполняем строку szMessage какой-либо информацией и вычисляем её длину sLen. Функция InvalidateRect отвечает за перерисовывание экрана, а значит передаёт ему сообщение WM_PAINT. Так как содержимое строки изменилось, на экране будет новое значение. Вот синтаксис функции перерисовки:

InvalidateRect(hWnd, NULL, TRUE);

Этот приём всегда очень выручает. Сообщение WM_PAINT остаётся нетронутым, меняются только числа введённые пользователем. А график рисуется совсем по-иному!

Теперь о служебных функциях, которые мы использовали в нашей программе. Раз уже мы о них заговорили...

1. Дисковые операции.
case IDM_DSPACE:
GetDiskFreeSpace(NULL, &lSectInClust, &lByteInSect, &lNumFreeClust, &lTotal);
lBuf1=lSectInClust*lByteInSect*lTotal;
sLen=sprintf(szMessage, "Всего на диске: %ld Мб", lBuf1/1000000);
break;

Выше были объявлены переменные: lSectInClust (секторов в кластере), lByteInSect (байт в секторе), lNumFreeClust (число свободных кластеров), lTotal (всего число кластеров). Функция GetDiskFreeSpace заполнила эти переменные реальными значениями этих параметров вашего жёсткого диска. lBuf - буфер, в который мы записали произведение: количество кластеров в секторе * число байт в кластере * общее число кластеров. Таким образом lBuf будет содержать количество байт на жёстком диске. Если изменить в формуле общее число кластеров на общее число свободных кластеров, мы узнаем, сколько свободных байт на винчестере.

2.Текущая папка
GetCurrentDirectory(MAX_PATH, szCurrent);
sLen=sprintf(szMessage, "Тек. папка %s", szCurrent);

Как мы уже говорили, MAX_PATH - константа, в которую записана длина максимально допустимого пути в Windows. Функция GetCurrentDirectory записывает в строку szCurrent текущий путь. Функция полностью поддерживает длинные имена, в отличие от старых API функций в Win16. В старых программах, написанных на Visual C++ 1.0, часто при добавлении какой-то папки можно увидеть дерево своего диска с обрубленными путями.

3.Системное время
case IDM_TIME:
GetSystemTime(&st);

min=st.wMinute;
sec=st.wSecond;
hour=st.wHour;
if(min<10)
sLen=sprintf(szMessage, "Время: %d:0%d:%d", hour, min, sec);
else
sLen=sprintf(szMessage, "Время: %ld:%d:%d", hour, min, sec);
break;

Структура, отвечающая за время прописана в библиотеке time.h. Она называется SYSTEMTIME. Создав экземпляр этой структуры, мы заполняем его поля функцией GetSystemTime. Затем мы присваиваем нашим переменным min, hour, sec значения этих полей: st.wMinute, st.wSecond, st.wHour. Если число минут меньше десяти, время будет отражаться так: 1:9:27, что неприемлемо. Поэтому неплохо будет отследить число минут меньшее 10 и добалять нолик. Можно это сделать и для секунд, но они не так бросаются в глаза.
С датой всё то же самое, только используем другие поля: st.wYear, st.wMonth. Кстати есть ещё поля: st.wDayOfWeek (день недели), st.wMilliseconds - число милисекунд.

4. Переменные окружения. Окружением называются окружающие нас важные данные: имя пользователя (в многопользовательских системах типа Windows 2000 (NT) это очень актуально), имя папки временных файлов Temp; имя компьютера, которое ему дали при установке Windows, путь в котором размещена папка Windows... Все эти данные используются серьёзными программами, которые привносят в систему свои драйвера, библиотеки DLL, файлы инициализации INI, и им просто необходимо знать путь системной папки. Тем не менее, получить доступ к этим данными предельно просто. Рассмотрим работу с такими функциями на примере получения имени компьютера.

char ComputerName[MAX_COMPUTERNAME_LENGTH + 1];
unsigned long len_ComputerName = MAX_COMPUTERNAME_LENGTH + 1; //длина имени
BOOL comp; //признак того, можно ли получить эту информацию

...
case IDM_ACOMP: //выбрали соответствующий пунктик меню

comp = GetComputerName(ComputerName, &len_ComputerName);

if( comp != 0 ) //Если информация доступна,
{
sLen=sprintf(szMessage, "%s", ComputerName); //формируем строку
}
else
//сообщение об ошибке, если нет доступа к переменным окружения
MessageBox(hWnd, "Имя компьютера не доступно!", "Сообщение:", MB_OK|MB_ICONSTOP);

break;


КАК работают акселлераторы клавиатуры?

Совсем недавно мы работали с клавиатурой и обрабатывали сообщения: WM_KEYDOWN, WM_CHAR и другие. Там мы сталкивались с понятием виртуальных клавиш, и я привёл таблицу кодов этих клавиш. Например ENTER обозначался, как VK_RETURN, ESC - VK_ESCAPE и.т.д.
Оказывается, можно очень просто связать конкретный идентификатор меню (IDM_EXIT, IDM_OPEN) с какой-нибудь виртуальной клавишей: Ctrl+Q, Ctrl+O...
Добавим в проект новый ресурс, нажав Ctrl+R. В окне выбора ресурса выберем первую строчку Accelerator.
Во влкадке ресурсов нашего приложения появится папочка Accelerator, в которой будет новый для всех нас ресурс: IDR_ACCELERATOR1. Первым делом переименуем его в IDR_MYACCEL. Считается плохим тоном программирования оставлять имена элементов управления, диалоговым окон и файлов, данные автоматически. Попробуй разобраться в программе с двадцатью окнами, которые называются IDD_DIALOG1, IDD_DIALOG2,... IDD_DIALOGN...
Теперь щёлкнем дважды по имени IDR_MYACCEL.
У нас появилось окно редактирования акселлераторов.

Пользоваться им предельно просто. Мы просто щёлкаем по пустой строке и в диалоговом окне выбираем идентификатор ресурса типа IDM_EXIT и сочетание клавиш, вызывающее его.
Дважды щёлкните мышью по пустой строке в таблице. Появится окно свойств акселлератора Accel Properties:


Щёлкните на вкладку ID окна Accel Properties. У вас появится длинный список идентификаторов, среди которых есть даже такие, которых вы не объявляли. Выберите например IDM_EXIT.
Ниже есть вкладка Key. В ней можно выбрать виртуальную клавишу: VK_RETURN, VK_ESCAPE... или задать её самому. В группе флажков Modifiers определяется сочетание: Alt, Ctrl или Shift. Оставьте флажок на Ctrl и нажмите кнопку Next Key Typed. Появится окно, предлагающее опробовать вашу комбинацию.

Нажмите Ctrl+Q - то сочетание, которое мы решили задать. Окно распознаёт все сочетания клавиш. Оно автоматически переставит переключатель на Alt или Shift, если вы нажмёте комбинацию с ними.
Теперь, когда все поля окна свойств акселлератора заполнены, закройте его. В таблице виртуальных клавиш добавилась строчка, в которой есть имя вашего идентификатора и соответствующие ему клавиши. Удобно, не правда ли?



Назад Главная Вперёд