15 мая 2023 года "Исходники.РУ" отмечают своё 23-летие!
Поздравляем всех причастных и неравнодушных с этим событием!
И огромное спасибо всем, кто был и остаётся с нами все эти годы!

Главная Форум Журнал Wiki DRKB Discuz!ML Помощь проекту


Как создавать значки в панели управления.

Автор: Paul DiLascia

Скачать исходник к статье - 179 Кб (Компилятор: Visual C++)

Значки, которые мы привыкли видеть в панели управления, это так называемые апплеты, которые представляют из себя обычные DLL-ки, имеющие расширение .cpl и содержащие в себе специфическую функцию CPlApplet. Каждый раз, когда запускается приложение панели управления (CONTROL.EXE), то сперва оно ищет в системной директории все файлы XXX.cpl, затем загружает каждую DLL и вызывает функцию CPlApplet с различными сообщениями. Например, когда Панель управления запускается первый раз, то функция CPlApplet вызывается с сообщением msg=CPL_INIT. Затем, если пользователь дважды кликнет по иконке аплета, то CPlApplet будет вызвана с сообщением msg=CPL_DBLCLK.

Каждая DLL-ка панели управления может поддерживать несколько иконок или апплетов. Для этого панель управления посылает сообщение CPL_GETCOUNT, и от нас требуется сообщить ей точное количество. После этого панель управления запросит информацию о каждом апплете при помощи сообщений CPL_INQUIRE или CPL_NEWINQUIRE. На рисунке показан процесс посылки сообщений панелью управления:


Рисунок 1.

Процедура общения Вашей DLL с панелью управления довольно универсальна и легко воплощается в классах. Поэтому я создал два класса CControlPanelApp и CCPApplet, которые собственно и занимаются процессом общения. Чтобы показать, как это работает, я написал собственную DLL панели управления MyPanel. Она включает в себя два апплета (Рисунок 2), один диалог (Рисунок 3) и одно окошко с закладками (Рисунок 4)


Рисунок 2.

MyPanel.cpp представляет обычное MFC Документ/Вид приложение за исключением того, что класс наследован от CControlPanelApp вместо CWinApp. А вместо InitInstance (которая обычно используется для добавления шаблонов документов) я вызываю OnInit, в которой создаю два апплета:

 BOOL CMyControlPanelApp::OnInit()
 {
     AddApplet(newCCPApplet(
         IDR_MYAPPLET1, 
         RUNTIME_CLASS(
         CMyDialog1)));
     AddApplet(new CCPApplet(
         IDR_MYAPPLET3, 
         RUNTIME_CLASS(
         CMyPropSheet)));
    return CControlPanelApp::OnInit();
 }

 

Класс апплета CCPApplet настолько универсален, что даже нет необходимости в MyPanel наследовать от него собственный. Единственное, что прийдётся дописать соственно код для диалогов. В моём случае, MyPanel включает диалог (CMyDialog) и property sheet (CMyPropSheet). Чтобы добавить свои диалоги, достаточно написать и и переопределить CControlPanelApp::OnInit как показано выше. Класс сделает всё остальное самостоятельно.


Рисунок 3.

Классы CControlPanelApp и CCPAppletBut так же заботятся о иконках, описании, функции CPlApplet а так же о всех CPL сообщениях. CPanel.cpp содержит в себе функцию CPlApplet, которая передаёт CPL сообщения в виртуальную функцию. Когда панель управления вызывает CPlApplet с сообщением CPL_INIT, то CPlApplet вызывает CControlPanelApp:: OnCplMsg, которая в свою очередь вызывает CControlPanelApp::OnInit. OnCplMsg это аналог CWnd::WindowProc, а OnInit - аналогичен обработчику сообщения OnCreate. Некоторые CPL сообщения, типа CPL_INQUIRE и CPL_ DBLCLK, имеют параметр lParam1, который содержит номер апплета (индекс), для которого предназначено сообщение. Как я уже говорил, DLL-ка панели управления может обслуживать несколько иконок или апплетов, поэтому в таких случаях CControlPanelApp::OnCplMsg направляет сообщение в виртуальную функцию в CCPApplet, а не CControlPanelApp.


Рисунок 4.

А теперь предлагаю более подробно разобраться с моим апплетом. Для создания апплета, вызывается конструктор, в который необходимо передать ID ресурса и MFC runtime class.

AddApplet(new CCPApplet(IDR_MYAPPLET3, RUNTIME_CLASS(CMyPropSheet)));

Этой информации достаточно для создания апплета. После вызова этой функции, Ваш апплет добавится к списку m_lsApplets. Дефолтовый обработчик для CPL_ GETCOUNT возвращает число апплетов, беря информацию именно из этого списка. Как только панель управления пошлёт CPL_INQUIRE или CPL_ NEWINQUIRE, то CCPApplet воспользуется идентификатором (ID) ресурса, чтобы получить иконку, имя и описание. Имя и описание, разделены на подстроки в основной строке ресурса.

 
STRINGTABLE PRELOAD DISCARDABLE 
 BEGIN
   IDR_MYAPPLET3  "Intergalactic\n
     Intergalactic settings for space  
     cadets\n\n"
 END

Теперь, если кликнуть по иконке апплета, то панель управления пошлёт сообщение CPL_DBLCKT, которое будет обработано функцией CCPApplet::OnLaunch, которая использует runtime class для создания экземпляра диалогового окошка или окна с закладками, а затем просто вызывает DoModal.

 LRESULT CCPApplet::OnLaunch(CWnd* pWndCpl, 
                            LPCSTR lpCmdLine)
 {
    CWnd* pw = (CWnd*)m_pDialogClass->CreateObject();
    if (pw) {
       if (pw->IsKindOf(RUNTIME_CLASS(CPropertySheet))) {
          CPropertySheet* ps = (CPropertySheet*)pw;
          ps->SetActivePage(lpCmdLine ? atoi(lpCmdLine) : 0);
          ps->DoModal();
       } else if (pw->IsKindOf(RUNTIME_CLASS(CDialog))) {
          CDialog* pd = (CDialog*)pw;
          pd->DoModal();
       }
    }
    return pw==NULL;
 }

Не забудьте объявить своё диалоговое окошко в DECLARE_DYNCREATE, иначе оно не создастся. Так же не забудьте переопределить OnPostNcDestroy чтобы "удалить его". Почему ? Объясняю. Обычно мы создаём диалог в стеке

   CMyDialog dlg;
   dlg.DoModal();

поэтому нет необходимости его удалять. Однако, CCPApplet создаёт Ваш диалог в куче, поэтому необходимо удалять его после того, как он будет уничтожен. Иначе будет утечка памяти.

После того, как апплет будет откомпилирован, не забудьте переименовать его в .cpl и поместить в системную директорию. Однако, DLL-ку можно оставить и в своей директории, тогда необходимо в CONTROL.INI в секции MMCPL добавить следующую строчку:

 [MMCPL]
 MyPanel=c:\utils\MyPanel\MyPanel.cpl

Существует маленькая проблемка, которая возникает, если Вы вдруг захотите добавить новый апплет или изменить имя или иконку. Изменения сразу не появятся в панели управления. Дело в том, что панель управления, после того как считает информацию (CPL_INQUIRE) из Вашего апплета, сражу же закэширует её на диск. Верный способ заставить панель управления поновой считать информацию из апплета, это переименовать DLL. Можно канечно просто нажать F5 (Обновить), но у меня это не дало результатов. В процессе разработки можно установить CCPApplet::m_bDynamic в TRUE, тем самым указав классу использовать CPL_NEWINQUIRE (информация не кэшируется) вместо CPL_INQUIRE (информация кэшируется). А после того, как все отладки будут закончены опять вернуть m_bDynamic=FALSE (по умолчанию).

Один из немаловажных вопросов, которые могут возникнуть при создании апплета панели управления, это как его отлаживать ? Есть два пути решения данной проблемы. Можно запустить панель управления под отладчиком, а можно воспользоваться rundll32:

 rundll32 shell32.dll,Control_RunDLL mypanel.cpl

Control_RunDLL, это специальная функция в shell32.dll, которая запускает приложение панели управления. Чтобы запустить определённый апплет в Вашей DLL, наберите следующее

 
  rundll32   
   shell32.dll,Control_RunDLL 
   mypanel.cpl,@n

где n, это номер Вашего апплета. Если добавить в конец строку, то она будет передана в CPL_ STARTWPARAMS типа командной строки (command line), которая передаётся  в стандартном приложении Windows. Обычно такая строка используется для апплетов, основанных на property sheet, чтобы сразу показать определённую страницу. Например, чтобы показать закладку Настройка (Settings) в свойствах экрана (Display Properties) наберите следующее:

 
   rundll32    
   shell32.dll,Control_RunDLL  
   desk.cpl,,3

В моей программе, нет необходимости делать дополнительную разборку параметров. Если Ваш апплет будет основан на property sheet, то CCPApplet автоматически интерпретирует дополнительный аргумент как номер страницы.

 // В CCPApplet::OnLaunch
 CWnd* pw = (CWnd*)m_pDialogClass->CreateObject();
 if (pw) {
    if (pw->IsKindOf(RUNTIME_CLASS(CPropertySheet))) {
       CPropertySheet* ps = (CPropertySheet*)pw;
       ps->SetActivePage(lpCmdLine ? 
                            atoi(lpCmdLine) : 0);
       ps->DoModal();
    }
 }