Blog. Just Blog

Меню с иконками на Ассемблере

Версия для печати Добавить в Избранное Отправить на E-Mail | Категория: Образ мышления: Assembler | Автор: ManHunter
Сегодня разберем кастомизацию такого элемента интерфейса, как выпадающее меню. Без меню обходится мало какое современное приложение, но вот работа с меню стандартными средствами обычно ограничивается установкой флага чекбокса, затенением неактивных пунктов или отрисовкой субменю. Попытка разнообразить меню, например, своими иконками, приводит к очень печальному результату. Ситуацию особо не спасают ни собственные битмапы, ни подгрузка изображений из списка ImageList. Очень странно, что на протяжении многих лет разработчики Windows так и не сделали инструментов "из коробки", чтобы можно было легко и комфортно работать с менюшками. К счастью, в системе есть "потайной ход", с помощью которого можно кастомизировать меню так, как вам захочется. Для своих проектов я постарался сделать как можно более универсальный инструмент для работы с меню и сейчас я с вами им поделюсь.

Начнем создание собственных элементов интерфейса с того, что для каждого настраиваемого меню резервируется структура с данными следующего формата:
  1. struct MYMENU
  2.   hMenu      dd ?
  3.   itemWidth  dd ?
  4.   itemHeight dd ?
  5.   hasIcons   db ?
  6. ends
В это же время, все меню, которые требуют кастомизации, формируют массив структур и указателей на них. Список заканчивается нулевым DWORD'ом:
  1. menus   dd Menu1
  2.         dd Menu2
  3.         dd ?
  4.  
  5. Menu1   MYMENU
  6. Menu2   MYMENU
Описание полей структуры: hMenu - хэндл меню, itemWidth - ширина пункта меню в пикселах, itemHeight - высота пункта меню в пикселах, hasIcons - флаг, определяющий, будут или нет использоваться в меню иконки (1 - да, 0 - нет). Если иконки будут использоваться, то слева от текста пунктов меню резервируется место для них, в противном случае текст пункта будет отрисовываться прямо от края меню.

Если вы хотите использовать значения размеров меню, соответствующие стандартным, то для этих целей описываются две константы с дефолтными значениями, определенными опытным путем. Еще одна константа используется в качестве идентификатора заголовков. Естественно, вы можете заменить их на ваши значения:
  1. DEFAULT_ITEM_WIDTH  = 80
  2. DEFAULT_ITEM_HEIGHT = 18
  3.  
  4. DEFAULT_HEADER_ID   = 9999
Для хранения иконок используется массив из пар DWORD'ов, определяющих ID пункта меню и хэндл иконки, который ему соответствует. Можно обернуть это в какую-нибудь структуру, но лично мне кажется это лишним.
  1. ICONS_COUNT = 100
  2. hIcons  rd ICONS_COUNT*2
Теперь ненадолго отвлечемся на теорию. Для того, чтобы можно было использовать собственный обработчик для кастомизации меню, его пункты должны быть добавлены с использованием флага MFT_OWNERDRAW. Перед отрисовкой такого пункта меню система отправляет окну-владельцу сообщение WM_MEASUREITEM. В lParam этого сообщения передается указатель на структуру MEASUREITEMSTRUCT, в которую, при необходимости, мы можем внести свои изменения, а именно установить ширину и высоту пунктов меню. Непосредственно при отрисовке пункта меню окну отправляется сообщение WM_DRAWITEM. В lParam сообщения передается указатель на структуру DRAWITEMSTRUCT, из которой мы можем получить все необходимые значения, например, текст пункта меню или состояние чекбокса, а также самостоятельно нарисовать пункт меню в том виде, в каком нам надо.

На этом можно заканчивать с теорией и переходить непосредственно к программированию. Начнем с создания меню. Обычно это делается на этапе инициализации окна. Создадим два меню с разными характеристиками.
  1.         ; Очистить память под массив иконок
  2.         invoke  RtlZeroMemory,hIcons,ICONS_COUNT*2*4
  3.         ; EDI -> указатель на массив иконок
  4.         mov     edi,hIcons
  5.  
  6.         ; Создать первое меню
  7.         invoke  CreatePopupMenu
  8.         ; Сохранить хэндл меню в структуру
  9.         mov     [Menu1.hMenu],eax
  10.         ; Установить ширину меню 120 пикселов
  11.         mov     [Menu1.itemWidth],120
  12.         ; В меню используются иконки
  13.         mov     [Menu1.hasIcons],TRUE
  14.         mov     ebx,eax
  15.  
  16.         ; Добавить заголовок меню
  17.         invoke  AppendMenu,ebx,MF_STRING+MFT_OWNERDRAW+MF_GRAYED,\
  18.                 DEFAULT_HEADER_ID,szStr10
  19.  
  20.         ; Сохранить ID пункта меню в массив с иконками
  21.         mov     eax,IDM_MENU11
  22.         stosd
  23.         ; Загрузить иконку из ресурсов
  24.         invoke  GetModuleHandle,0
  25.         invoke  LoadIcon,eax,1
  26.         ; Сохранить хэндл иконки в массив
  27.         stosd
  28.  
  29.         ; Добавить пункт в меню
  30.         invoke  AppendMenu,ebx,MF_STRING+MFT_OWNERDRAW+MF_GRAYED,\
  31.                 IDM_MENU11,szStr11
  32.  
  33.         ; Сохранить ID пункта меню в массив с иконками
  34.         mov     eax,IDM_MENU12
  35.         stosd
  36.         ; Загрузить иконку из ресурсов
  37.         invoke  GetModuleHandle,0
  38.         invoke  LoadIcon,eax,2
  39.         stosd
  40.  
  41.         ; Добавить пункт в меню
  42.         invoke  AppendMenu,ebx,MF_STRING+MFT_OWNERDRAW,IDM_MENU12,szStr12
  43.         ...
  44.         ; и так далее
  45.         ...
  46.  
  47.         ; Создать второе меню
  48.         invoke  CreatePopupMenu
  49.         mov     [Menu2.hMenu],eax
  50.         ; Ширина меню 60 пикселов, высота дефолтная
  51.         mov     [Menu2.itemWidth],60
  52.         mov     ebx,eax
  53.  
  54.         invoke  AppendMenu,ebx,MF_STRING+MFT_OWNERDRAW,IDM_MENU21,szStr21
  55.         invoke  AppendMenu,ebx,MF_STRING+MFT_OWNERDRAW+MF_CHECKED,\
  56.                 IDM_MENU22,szStr22
  57.         invoke  AppendMenu,ebx,MF_STRING+MFT_OWNERDRAW,IDM_MENU23,szStr23
  58.         invoke  AppendMenu,ebx,MF_SEPARATOR,NULL,NULL
  59.  
  60.         ; Создать субменю, которое не кастомизируется
  61.         invoke  CreatePopupMenu
  62.         mov     edi,eax
  63.         invoke  AppendMenu, edi,MF_STRING,IDM_MENU21,szStr21
  64.  
  65.         ; Добавить пункты меню
  66.         invoke  AppendMenu,ebx,MFT_OWNERDRAW+MF_POPUP,edi,szStr24
  67.         invoke  AppendMenu,ebx,MF_STRING+MFT_OWNERDRAW,IDM_MENU25,szStr25
Подобным образом в приложении создаются все менюшки, которые надо кастомизировать. Каждая описывается в своей структуре, в каждую добавляются пункты с соответствующими состояниями чекбоксов, статусом активности, иконками и текстами. Опционально самым первым пунктом можно добавить заголовок, но обязательно с флагом MF_GRAYED, чтобы меню не реагировало при клике на него. Если меню не описано в массиве, то оно будет выводиться на экран штатными средствами.

Переходим к обработчикам сообщений. Начнем с обработчика WM_MEASUREITEM, в котором будем возвращать размеры пункта меню в соответствии со значениями, которые мы задали при инициализации. Здесь нам надо определить, к какому меню принадлежит запрашиваемый пункт. Я так и не нашел способов сделать это одним движением, приходится перебирать все меню из массива по очереди и запрашивать информацию о пункте с нужным индексом. Если ошибки при этом не случилось, значит пункт принадлежит этому меню. Естественно, при такой реализации одинаковых индексов в разных меню быть не должно. Исключения составляют заголовки, они распознаются по индексу, заданному константой DEFAULT_HEADER_ID.
  1.         cmp     [msg],WM_MEASUREITEM
  2.         je      .wmmeasure
  3.         ...
  4.         ...
  5.         ...
  6. .wmmeasure:
  7.         ; Отрисовываем пункт меню?
  8.         mov     ebx,[lparam]
  9.         cmp     [ebx+MEASUREITEMSTRUCT.CtlType],ODT_MENU
  10.         jne     .processed
  11.  
  12.         ; Найти, какому меню принадлежит пункт
  13.         mov     [mii.cbSize],sizeof.MENUITEMINFO
  14.         mov     esi,menus
  15. .loop_measure:
  16.         lodsd
  17.         or      eax,eax
  18.         jz      .processed
  19.  
  20.         ; Пункт принадлежит этому меню?
  21.         mov     edi,eax
  22.         invoke  GetMenuItemInfo,[edi+MYMENU.hMenu],\
  23.                 [ebx+MEASUREITEMSTRUCT.itemID],FALSE,mii
  24.         or      eax,eax
  25.         ; Ошибка, пункт меню не найден, проверяем следующий
  26.         jz      .loop_measure
  27.  
  28.         ; Ширина пункта меню
  29.         mov     eax,[edi+MYMENU.itemWidth]
  30.         or      eax,eax
  31.         jnz     @f
  32.         mov     eax,DEFAULT_ITEM_WIDTH
  33. @@:
  34.         mov     [ebx+MEASUREITEMSTRUCT.itemWidth],eax
  35.  
  36.         ; Высота пункта меню
  37.         mov     eax,[edi+MYMENU.itemHeight]
  38.         or      eax,eax
  39.         jnz     @f
  40.         mov     eax,DEFAULT_ITEM_HEIGHT
  41. @@:
  42.         ; Для заголовков меню дополнительные поля
  43.         cmp     [ebx+MEASUREITEMSTRUCT.itemID],DEFAULT_HEADER_ID
  44.         jne     @f
  45.         add     eax,4
  46. @@:
  47.         mov     [ebx+MEASUREITEMSTRUCT.itemHeight],eax
  48.         jmp     .processed
Алгоритм работы следующий: последовательно перебираются все хэндлы меню из массива, для каждого меню проверяется, принадлежит ли пункт этому меню или нет. Если принадлежит, то из соответствующей структуры загружается или установленные, или дефолтные значения размеров пунктов меню. К высоте заголовков добавляется несколько пикселов.

В обработчике сообщения WM_DRAWITEM действий гораздо больше. Здесь надо предусмотреть загрузку и отрисовку иконки, которая соответствует пункту меню, надо обработать флаг взведенного чекбокса и отрисовать соответствующую иконку, надо обработать фон для заблокированного, обычного и активного пункта меню, а также установить отдельный цвет и шрифт для заголовков.
  1.         cmp     [msg],WM_DRAWITEM
  2.         je      .wmdraw
  3.         ...
  4.         ...
  5.         ...
  6. .wmdraw:
  7.         ; Отрисовываем пункт меню?
  8.         mov     ebx,[lparam]
  9.         cmp     [ebx+DRAWITEMSTRUCT.CtlType],ODT_MENU
  10.         jne     .processed
  11.  
  12.         ; Это заголовок меню?
  13.         cmp     [ebx+DRAWITEMSTRUCT.itemID],DEFAULT_HEADER_ID
  14.         jne     .simple_text
  15.  
  16.         invoke  GetSysColor,COLOR_INACTIVECAPTIONTEXT
  17.         invoke  SetTextColor,[ebx+DRAWITEMSTRUCT.hDC],eax
  18.         invoke  GetSysColor,COLOR_INACTIVECAPTION
  19.         mov     esi,eax
  20.         invoke  SetBkColor,[ebx+DRAWITEMSTRUCT.hDC],esi
  21.  
  22.         invoke  CreateSolidBrush,esi
  23.         mov     [hBrush],eax
  24.         invoke  SelectObject,[ebx+DRAWITEMSTRUCT.hDC],eax
  25.  
  26.         invoke  CreatePen,PS_INSIDEFRAME,0,esi
  27.         mov     [hPen],eax
  28.         invoke  SelectObject,[ebx+DRAWITEMSTRUCT.hDC],eax
  29.  
  30.         jmp     .draw_menu_row
  31.  
  32. .simple_text:
  33.  
  34.         test    [ebx+DRAWITEMSTRUCT.itemState],ODS_SELECTED
  35.         jnz     .selected_text
  36. .normal_text:
  37.         invoke  GetSysColor,COLOR_MENUTEXT
  38.         ; Пункт меню неактивен?
  39.         test    [ebx+DRAWITEMSTRUCT.itemState],ODS_GRAYED+ODS_DISABLED
  40.         jz      @f
  41.         invoke  GetSysColor,COLOR_GRAYTEXT
  42. @@:
  43.         invoke  SetTextColor,[ebx+DRAWITEMSTRUCT.hDC],eax
  44.         invoke  GetSysColor,COLOR_MENU
  45.         mov     esi,eax
  46.         invoke  SetBkColor,[ebx+DRAWITEMSTRUCT.hDC],esi
  47.  
  48.         invoke  CreateSolidBrush,esi
  49.         mov     [hBrush],eax
  50.         invoke  SelectObject,[ebx+DRAWITEMSTRUCT.hDC],eax
  51.  
  52.         invoke  CreatePen,PS_INSIDEFRAME,0,esi
  53.         mov     [hPen],eax
  54.         invoke  SelectObject,[ebx+DRAWITEMSTRUCT.hDC],eax
  55.  
  56.         jmp     .draw_menu_row
  57.  
  58. .selected_text:
  59.         invoke  GetSysColor,COLOR_HIGHLIGHTTEXT
  60.         ; Пункт меню неактивен?
  61.         test    [ebx+DRAWITEMSTRUCT.itemState],ODS_GRAYED+ODS_DISABLED
  62.         jz      @f
  63.         invoke  GetSysColor,COLOR_GRAYTEXT
  64. @@:
  65.         invoke  SetTextColor,[ebx+DRAWITEMSTRUCT.hDC],eax
  66.  
  67.         invoke  GetSysColor,COLOR_HIGHLIGHT
  68.         ; Пункт меню неактивен?
  69.         test    [ebx+DRAWITEMSTRUCT.itemState],ODS_GRAYED+ODS_DISABLED
  70.         jz      @f
  71.         invoke  GetSysColor,COLOR_MENU
  72. @@:
  73.         mov     esi,eax
  74.         invoke  SetBkColor,[ebx+DRAWITEMSTRUCT.hDC],esi
  75.  
  76.         invoke  CreateSolidBrush,esi
  77.         mov     [hBrush],eax
  78.         invoke  SelectObject,[ebx+DRAWITEMSTRUCT.hDC],eax
  79.  
  80.         invoke  CreatePen,PS_INSIDEFRAME,0,esi
  81.         mov     [hPen],eax
  82.         invoke  SelectObject,[ebx+DRAWITEMSTRUCT.hDC],eax
  83.  
  84. .draw_menu_row:
  85.         ; Это заголовок меню?
  86.         cmp     [ebx+DRAWITEMSTRUCT.itemID],DEFAULT_HEADER_ID
  87.         jne     .draw_text
  88.  
  89. .draw_header:
  90.         sub     [ebx+DRAWITEMSTRUCT.rcItem.bottom],2
  91.  
  92.         ; Прямоугольник с заливкой
  93.         invoke  Rectangle,[ebx+DRAWITEMSTRUCT.hDC],\
  94.                 [ebx+DRAWITEMSTRUCT.rcItem.left],\
  95.                 [ebx+DRAWITEMSTRUCT.rcItem.top],\
  96.                 [ebx+DRAWITEMSTRUCT.rcItem.right],\
  97.                 [ebx+DRAWITEMSTRUCT.rcItem.bottom]
  98.  
  99.         invoke  DeleteObject,[hBrush]
  100.         invoke  DeleteObject,[hPen]
  101.  
  102.         sub     [ebx+DRAWITEMSTRUCT.rcItem.left],1
  103.  
  104.         ; Жирный шрифт
  105.         invoke  GetStockObject,ANSI_VAR_FONT
  106.         invoke  GetObject,eax,sizeof.LOGFONT,logFont
  107.         mov     [logFont.lfWeight],FW_BLACK
  108.         invoke  CreateFontIndirect,logFont
  109.         mov     [hFont],eax
  110.         invoke  SelectObject,[ebx+DRAWITEMSTRUCT.hDC],eax
  111.         mov     [hOldFont],eax
  112.  
  113.         add     [ebx+DRAWITEMSTRUCT.rcItem.left],2
  114.         invoke  lstrlen,[ebx+DRAWITEMSTRUCT.itemData]
  115.         lea     ecx,[ebx+DRAWITEMSTRUCT.rcItem]
  116.         invoke  DrawText,[ebx+DRAWITEMSTRUCT.hDC],\
  117.                 [ebx+DRAWITEMSTRUCT.itemData],\
  118.                 eax,ecx,DT_SINGLELINE+DT_VCENTER+DT_CENTER
  119.  
  120.         invoke  DeleteObject,[hFont]
  121.  
  122.         invoke  SelectObject,[ebx+DRAWITEMSTRUCT.hDC],[hOldFont]
  123.  
  124.         jmp     .processed
  125.  
  126. .draw_text:
  127.         ; Прямоугольник с заливкой
  128.         invoke  Rectangle,[ebx+DRAWITEMSTRUCT.hDC],\
  129.                 [ebx+DRAWITEMSTRUCT.rcItem.left],\
  130.                 [ebx+DRAWITEMSTRUCT.rcItem.top],\
  131.                 [ebx+DRAWITEMSTRUCT.rcItem.right],\
  132.                 [ebx+DRAWITEMSTRUCT.rcItem.bottom]
  133.  
  134.         invoke  DeleteObject,[hBrush]
  135.         invoke  DeleteObject,[hPen]
  136.  
  137.         add     [ebx+DRAWITEMSTRUCT.rcItem.left],1
  138.  
  139.         ; Найти, какому меню принадлежит пункт
  140.         mov     [mii.cbSize],sizeof.MENUITEMINFO
  141.         mov     [mii.fMask],MIIM_STATE
  142.         mov     esi,menus
  143. .loop_draw:
  144.         lodsd
  145.         or      eax,eax
  146.         jz      .no_icon_padding
  147.  
  148.         ; Пункт принадлежит этому меню?
  149.         mov     edi,eax
  150.         invoke  GetMenuItemInfo,[edi+MYMENU.hMenu],\
  151.                 [ebx+DRAWITEMSTRUCT.itemID],FALSE,mii
  152.         or      eax,eax
  153.         jnz     .chk_state
  154.  
  155.         invoke  GetLastError
  156.         cmp     eax,ERROR_MENU_ITEM_NOT_FOUND
  157.         je      .loop_draw
  158.         jmp     .no_icon_padding
  159.  
  160. .chk_state:
  161.         ; Это чекбокс и он установлен?
  162.         test    [mii.fState],ODS_CHECKED
  163.         jz      .chk_icon
  164.  
  165.         mov     ecx,[edi+MYMENU.itemHeight]
  166.         or      ecx,ecx
  167.         jnz     @f
  168.         mov     ecx,DEFAULT_ITEM_HEIGHT
  169. @@:
  170.         sub     ecx,16
  171.         shr     ecx,1
  172.         push    ecx
  173.         add     [ebx+DRAWITEMSTRUCT.rcItem.top],ecx
  174.  
  175.         invoke  GetModuleHandle,0
  176.         invoke  LoadIcon,eax,77
  177.  
  178.         sub     [ebx+DRAWITEMSTRUCT.rcItem.right],18
  179.  
  180.         invoke  DrawIconEx,[ebx+DRAWITEMSTRUCT.hDC],\
  181.                 [ebx+DRAWITEMSTRUCT.rcItem.right],\
  182.                 [ebx+DRAWITEMSTRUCT.rcItem.top],\
  183.                 eax,16,16,NULL,NULL,DI_NORMAL+DI_COMPAT
  184.         pop     ecx
  185.         sub     [ebx+DRAWITEMSTRUCT.rcItem.top],ecx
  186.  
  187. .chk_icon:
  188.         ; Используется ли в меню иконка
  189.         cmp     [edi+MYMENU.hasIcons],TRUE
  190.         jne     .no_icon_padding
  191.  
  192.         ; Найти иконку, которая связана с пунктом меню
  193.         mov     esi,hIcons
  194. .find_icon:
  195.         ; ID пункта меню
  196.         lodsd
  197.         or      eax,eax
  198.         jz      .no_icon_found
  199.  
  200.         cmp     eax,[ebx+DRAWITEMSTRUCT.itemID]
  201.         je      @f
  202.  
  203.         ; Хэндл иконки
  204.         lodsd
  205.         jmp     .find_icon
  206. @@:
  207.         ; Хэндл иконки
  208.         lodsd
  209.  
  210.         mov     ecx,[edi+MYMENU.itemHeight]
  211.         or      ecx,ecx
  212.         jnz     @f
  213.         mov     ecx,DEFAULT_ITEM_HEIGHT
  214. @@:
  215.         sub     ecx,16
  216.         shr     ecx,1
  217.         push    ecx
  218.         add     [ebx+DRAWITEMSTRUCT.rcItem.top],ecx
  219.         invoke  DrawIconEx,[ebx+DRAWITEMSTRUCT.hDC],\
  220.                 [ebx+DRAWITEMSTRUCT.rcItem.left],\
  221.                 [ebx+DRAWITEMSTRUCT.rcItem.top],\
  222.                 eax,16,16,NULL,NULL,DI_NORMAL+DI_COMPAT
  223.         pop     ecx
  224.         sub     [ebx+DRAWITEMSTRUCT.rcItem.top],ecx
  225.  
  226. .no_icon_found:
  227.         add     [ebx+DRAWITEMSTRUCT.rcItem.left],18
  228.  
  229. .no_icon_padding:
  230.         add     [ebx+DRAWITEMSTRUCT.rcItem.left],2
  231.         invoke  lstrlen,[ebx+DRAWITEMSTRUCT.itemData]
  232.         lea     ecx,[ebx+DRAWITEMSTRUCT.rcItem]
  233.         invoke  DrawText,[ebx+DRAWITEMSTRUCT.hDC],\
  234.                 [ebx+DRAWITEMSTRUCT.itemData],\
  235.                 eax,ecx,DT_SINGLELINE+DT_VCENTER+DT_LEFT
  236.  
  237.         jmp     .processed
Конечно, десятки тысяч разработчиков и дизайнеры интерфейсов MicroSoft ошибаться не могут, но на мой взгляд нет большего мудачества, чем рисовать значок чекбокса слева от пункта меню вместо иконки этого пункта. Лично я считаю, что чекбокс должен находиться справа от текста пункта меню, чтобы не забивать основную иконку. Поэтому в моей реализации пользовательской отрисовки меню иконка чекбокса выводится справа, никак не влияя на основную иконку пункта меню.

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

Важное замечание. Строки заголовков и всех пунктов меню должны постоянно присутствовать в памяти. То есть нельзя, например, динамически сгенерировать меню из нескольких пунктов, используя один и тот же буфер для каждой строки. Для обычного меню такое запросто прокатывает, а для кастомного - нет.

Пример кастомизированного меню
Пример кастомизированного меню

В приложении пример программы, реализующей оба варианта вывода контекстного меню.

Пример программы с исходным текстом (FASM)Пример программы с исходным текстом (FASM)

Menu.Demo.zip (9,450 bytes)


Поделиться ссылкой ВКонтакте
Просмотров: 3208 | Комментариев: 3

Метки: Assembler, иконки
Внимание! Статья опубликована больше года назад, информация могла устареть!

Комментарии

Отзывы посетителей сайта о статье
mihail (25.01.2022 в 23:18):
Спасибо! Отличный материал!
ManHunter (15.12.2021 в 10:59):
Выплыла ситуация, когда при определении принадлежности пункта меню после вызова GetMenuItemInfo появлялась не ошибка ERROR_MENU_ITEM_NOT_FOUND, а совершенно другое. Так что ограничился проверкой результата из GetMenuItemInfo, этого достаточно. Статью и исходники обновил.
ManHunter (30.05.2018 в 14:58):
Давно хотел добавить сюда заголовок меню, наконец сделал. Статья и код доработаны, архив с примером перезалил.

Добавить комментарий

Заполните форму для добавления комментария
Имя*:
Текст комментария (не более 2000 символов)*:

*Все поля обязательны для заполнения.
Комментарии, содержащие рекламу, ненормативную лексику, оскорбления и т.п., а также флуд и сообщения не по теме, будут удаляться. Нарушителям может быть заблокирован доступ к сайту.
Наверх
Powered by PCL's Speckled Band Engine 0.2 RC3
© ManHunter / PCL, 2008-2024
При использовании материалов ссылка на сайт обязательна
Время генерации: 0.12 сек. / MySQL: 2 (0.0099 сек.) / Память: 4.5 Mb
Наверх