корявый/вырвиглазный интерфейс

Тема в разделе "WASM.GDI+", создана пользователем Mikl___, 25 янв 2025 в 15:38.

  1. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    3.864
    Создаю простейшую программу, выводящую на экран пунктирную стрелку.
    Код (ASM):
    1. ; GUI #
    2. include win64a.inc
    3. UnitWorld equ 0
    4. LineCapRoundAnchor equ 12h
    5. LineCapArrowAnchor equ 14h
    6. .code
    7. WinMain proc
    8. local msg:MSG
    9. ;инициализация библиотеки GDIPLUS
    10.        invoke GdiplusStartup,&gdiplusToken,&gsi,0
    11.        xor ebx,ebx
    12.        mov esi,IMAGE_BASE
    13.        invoke LoadCursorFromFileA,"br_Rabbit3.cur"
    14.        mov edi,offset ClassName
    15.        push rax    ;hIconSm
    16.        push rdi    ;lpszClassName
    17.        push rbx    ;lpszMenuName
    18.        push COLOR_WINDOW  ;hbrBackground
    19.        push rax    ;hCursor
    20.        push rax       ;hIcon
    21.        push rsi    ;hInstance
    22.        push rbx       ;cbClsExtra & cbWndExtra
    23.        pushaddr WndProc     ;lpfnWndProc
    24.        push sizeof WNDCLASSEX;cbSize & style
    25.        invoke RegisterClassExA,esp ;addr WNDCLASSEX
    26.        push rbx
    27.        push rsi ;rsi=400000h
    28.        shl esi,9 ;rsi=CW_USEDEFAULT
    29.        push rbx
    30.        push rbx
    31.        push 100
    32.        push 240
    33.        push rsi
    34.        push rsi
    35.        sub esp,20h
    36.        invoke CreateWindowExA,0,edi,edi,WS_OVERLAPPEDWINDOW or WS_VISIBLE
    37.        lea edi,msg
    38. @@:    invoke GetMessageA,edi,NULL,0,0
    39.        invoke DispatchMessageA,edi
    40.        jmp @b
    41. WinMain endp
    42. WndProc proc hWnd:QWORD,uMsg:QWORD,wParam:QWORD,lParam:QWORD
    43.        mov hWnd,rcx
    44.        cmp edx,WM_DESTROY
    45.        je wmDESTROY
    46.        cmp edx,WM_CREATE
    47.        je wmCREATE
    48.        cmp edx,WM_PAINT
    49.        je wmPAINT
    50.        leave
    51.        jmp NtdllDefWindowProc_
    52. wmDESTROY:;уничтожить перо
    53.        invoke GdipDeletePen,hPen
    54. ;уничтожить объект GDI+
    55.        invoke GdipDeleteGraphics,graphics
    56.        mov ecx,gdiplusToken
    57.        invoke GdiplusShutdown
    58.        invoke RtlExitUserProcess,NULL
    59. ;------------------------------------------------------------
    60. wmPAINT:;Прерывистая линия
    61.        mov r9d,20
    62.        mov [rsp+28h],r9
    63.        mov qword ptr[rsp+20h],200
    64.        invoke GdipDrawLineI,graphics,hPen0,50
    65.        jmp wmBYE
    66. ;------------------------------------------------------------
    67. wmCREATE:invoke GdipCreateFromHWND,hWnd,&graphics
    68.        movd xmm1,const8
    69.        invoke GdipCreatePen1,0FFFF0000h,,UnitWorld,&hPen
    70.        invoke GdipSetPenStartCap,hPen,LineCapRoundAnchor
    71.        invoke GdipSetPenEndCap,hPen,LineCapArrowAnchor
    72.        invoke GdipSetPenDashArray,hPen,&dash,2
    73. wmBYE: leave
    74.        ret
    75. WndProc endp
    76. ;---------------------------------------
    77. .data
    78. ClassName db 'GDI+ рисование линий',0
    79. gsi GdiplusStartupInput <1,0,0,0,0>
    80. const8 dd 8.0
    81. dash dd 1.0,0.5
    82. .data?
    83. graphics dq ?
    84. hPen dq ?
    85. gdiplusToken dd ?
    86. hDC dq ?
    87. end
    Если кому-то не нравится оформление программы, пожалуйста напишите почему. А теперь попробую объяснить каждую строку.

    Перед тем как использовать GDI+, необходимо инициализировать библиотеку gdiplus.dll для этого используется функция GdiplusStartup, 1-ый параметр -- адрес двойного слова, куда будет помещен маркер gdiplusToken, который затем следует использовать при закрытии библиотеки функцией GdiplusShutdown.
    Создаю в стеке структуру типа WNDCLASSEX и передаю адрес вершины стека функции RegisterClassEx. Четыре первые параметра передаю через регистры RCX, RDX, R8, R9 для функции CreateWindowEx, остальные параметры через push YY, хотя можно и через mov [rsp+XXh],YY но так нагляднее и меньше писанины :)
    Функции WinAPI не изменяют значения в регистрах RSP, RBP, RSI, RDI, RBX, R12-R15, XMM6-XMM15. Поэтому я использую регистр RBX чтобы передать нулевые значения функциям RegisterClassEx и CreateWindowEx, а регистр RDI сперва, чтобы передать функциям RegisterClassEx и CreateWindowEx адрес класса окна, а имя класса у меня совпадает с именем окна, а затем через RDI передаю адрес msg функциям GetMessage и DispatchMessage. Это позволяет использовать 1-байтную команду push RBX вместо 2-байтной push 0 и 1-байтную команду push RDI вместо 5-байтной push адрес. Для получения дескриптора экземпляра приложения не вызываю функцию GetModuleHandle, так как при создании приложения используется ключ /BASE:0x400000 и поэтому всегда hInstance=400000h, это значение помещается в RSI mov esi,IMAGE_BASE дважды использую push RSI для RegisterClassEx и CreateWindowEx, а когда для CreateWindowEx требуется CW_USEDEFAULT=80000000h сдвигаю содержимое RSI на 9 разрядов влево (400000h×29=80000000h).
    При получении сообщения о закрытии окна не отправляю через PostQuitMessage сообщение WM_QUIT в очередь сообщений потока, чтобы GetMessage разорвала цикл, а через RtlExitUserProcess убиваю приложение.
    В приложении обрабатывается три сообщения WM_DESTROY, WM_CREATE и WM_PAINT:
    • После получения WM_CREATE создаем объект GDI+ graphics для вывода графической информации и связываем его с hWnd функцией GdipCreateFromHWND, далее функцией GdipCreatePen1 создается перо hPen красного цвета, толщиной 8 пикселей, один конец линии будет в виде окружности (GdipSetPenStartCap(hPen, LineCapRoundAnchor)), другой в виде стрелки (GdipSetPenEndCap(hPen, LineCapArrowAnchor)), сама линия будет штриховой (GdipSetPenDashArray(hPen, &dash, 2)), длина штриха 1×8=8 пикселей, расстояние между штрихами 0,5×8=4 пикселя (dash dd 1.0,0.5)
    • После получения WM_PAINT рисуем линию GdipDrawLineI(graphics, hPen) с координатами x1=50 y1=20 x2=200 y2=20
    • При получении WM_DESTROY уничтожаем перо GdipDeletePen, уничтожаем объект GDI+ GdipDeleteGraphics, закрываем библиотеку gdiplus.dll функцией GdiplusShutdown, завершаем приложение RtlExitUserProcess
    Если пришедшее сообщение не WM_DESTROY, не WM_CREATE и не WM_PAINT, тогда произойдет не call NtdllDefWindowProc_ (DefWindowProc) с теми же параметрами, что и для WndProc с последующими leave и ret, а просто jmp NtdllDefWindowProc_
    Пока разбирался с lib- и inc-файлами узнал, что ExitProcess в kernel32.dll это обертка для RtlExitUserProcess из ntdll.dll, а DefWindowProc в user32.dll обертка для NtdllDefWindowProc_ из ntdll.dll, поэтому решил обойтись без посредников...

    Вроде бы, ничего не упустил, а если непонятно ― спрашивайте
     

    Вложения:

    • Fig_06_08.png
      Fig_06_08.png
      Размер файла:
      2 КБ
      Просмотров:
      79
    mantissa нравится это.
  2. HoShiMin

    HoShiMin Well-Known Member

    Публикаций:
    5
    Регистрация:
    17 дек 2016
    Сообщения:
    1.472
    Адрес:
    Россия, Нижний Новгород
    Mikl___, а в чём концепция этих туториалов? Это больше туториал по GDI+ или по ассемблеру?
    Если больше по GDI+, то ассемблер - совсем не лучший выбор, поскольку задача должна быть максимально просто донести материал именно по API.
    Те, кто собирается писать на ассемблере, и так знают, как раскладывать регистры и организовывать функции - им важнее концепция: какие функции в каком порядке вызывать и что передавать в аргументы, а ассемблерную реализацию они смогут написать и сами.
    Ассемблер сильно усложняет восприятие, поскольку в примерах вместо логики приходится следить за множеством деталей - где что лежит, в каком состоянии у нас регистры и стек: это нас сильно отвлекает от самоцели - научиться использовать этот API.
    Саму концепцию лучше подавать на высокоуровневых языках - на C или C++, чтобы было кратко и наглядно: вот мы вызываем это, это и это, вот получаем результат.

    Кроме того, когда примеры на C/C++, мы видим, откуда что берётся - работаем с хедерами-первоисточниками.
    На ассемблере же сразу начинается магия. Например, в примере выше сразу идут две константы: LineCapRoundAnchor и LineCapArrowAnchor.
    Откуда взялись их магические значения? Где искать значения других констант, если они понадобятся читателю? Непонятно.
    Ради интереса пробуем даже загуглить - на MSDN их нет, а гуглятся только в исходниках документации на майкрософтовском гитхабе - это явно не тот источник, с которым захочет работать читатель.
    А когда видим пример на плюсах с хедерами - нет проблем, мы знаем: "Ага, взято вот оттуда".

    В общем, стоит отделять мух от котлет: уроки по API писать на высокоуровневых языках, а уроки именно по ассемблеру (какие-нибудь трюки, готовые примеры алгоритмов и библиотечные вещи) - да, их уже на асме.
     
    Mikl___ нравится это.
  3. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    3.864
    HoShiMin,
    во вложении файл GDIPLUS.INC
    в топике Введение в GDI+ я где мог расставил значения констант GDI+
     

    Вложения:

    • GDIPLUS.zip
      Размер файла:
      22,8 КБ
      Просмотров:
      12
  4. HoShiMin

    HoShiMin Well-Known Member

    Публикаций:
    5
    Регистрация:
    17 дек 2016
    Сообщения:
    1.472
    Адрес:
    Россия, Нижний Новгород
    Здесь речь о том, что даже если ты где-то описал константы или приложил .inc - это всё равно не первоисточник.
    Рано или поздно читатель захочет выйти за рамки - а значит, нужно его к этому подготовить: объяснить, где это искать в оригинале, чтобы, когда ему понадобится информация, которую не покрывают уроки, он знал не только, где её найти, но и почему нужно искать именно там.

    Первоисточники у нас - MSDN и сишные хедеры.
    Нет даже большого смысла их дублировать: ты же всё равно не сможешь портировать все функции и константы - но это и не нужно, если объяснить читателю, где их найти.
    Дальше он просто откроет хедер, возьмёт нужное и перенесёт к себе.

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

    Собственно, мой посыл в том, что ценность твоих уроков не в ассемблерных листингах, а в объяснении API доступным языком, а объяснять его лучше на языке оригинала.
    Вот читаю ту статью по введению в GDI+: всё хорошо, но мы словно начали с середины.
    Введение, а дальше сразу описание функций. Но до вызова функций у нас ещё целый пласт по созданию окна и оконной очереди. А этого как раз и нет.
    И главное - там примеры на плюсах, это очень хорошо (кстати, там у всего кода почему-то пропали отступы) - на плюсах и стоит продолжать: прям в аттач к каждой главе краткий примерчик полного приложения, как вот этот пример в первом посте.
    Ну и раз уж начали, ещё описания структур, энумов и констант вместо дублирования лучше сопровождать ссылками на MSDN.
    Типа такого:
    Список цветов описывает энум Color, значения которого можно посмотреть здесь [ссылка на MSDN].

    Так и статьи в объёме будут многократно короче, и сразу будет понятно, откуда что взялось.
     
    Mikl___ нравится это.
  5. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    3.864
    Парикмахерская. Молодая неопытная парикмахерша первый раз бреет клиента опасной бритвой. Ведет бритвой по щеке медленно и аккуратно, от усердия вся вспотела и язычок прикусила. Р-р-раз и порезала клиента: ― ой, извините! Я не хотела...
    ― ничего, ничего... успокойтесь, продолжайте...
    Еще один порез: ― ой, извините! Я не хотела...
    ― ничего, ничего... успокойтесь, продолжайте...
    Еще один порез: ― ой, извините! Я не хотела...
    Еще, еще и еще ...
    Разозленная парикмахерша несколько раз крест-накрест чиркает бритвой по лицу клиента: ― Ну, не получается у меня, не получается...
     
  6. HoShiMin

    HoShiMin Well-Known Member

    Публикаций:
    5
    Регистрация:
    17 дек 2016
    Сообщения:
    1.472
    Адрес:
    Россия, Нижний Новгород
    Неправда, всё получается)
    И вообще, это лишь моё мнение, может тут сейчас наоборот все скажут: «Не хотим си, форум про ассемблер - давай на ассемблере» :)
     
    Mikl___ нравится это.
  7. TrashGen

    TrashGen ТрещГен

    Публикаций:
    0
    Регистрация:
    15 мар 2011
    Сообщения:
    1.195
    Адрес:
    подполье
    db «Не хотим си, форум про ассемблер - давай на ассемблере»,0
    --- Сообщение объединено, 26 янв 2025 в 02:34 ---
    В полтретьего ночи проснулся, чтобы это сказать!
     
    HoShiMin и Mikl___ нравится это.
  8. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    3.864
    TrashGen,
    Хорошо, приступим.

    Общий алгоритм написания программ для GDI+

    • Получаем адрес PEB.mov rax,qword ptr gs:[60h]
    • Через PEB Ldr InMemoryOrderModuleList, найдем ntdll.dll.
      mov rax,[rax+18h];Смещаемся к структуре PEB_LDR_DATA
      mov rax,[rax+20h];Смещаемся к полю PEB_LDR_DATA.InMemoryOrderModuleList
      mov rax,[rax]
      mov rax,[rax+20h];Базовый адрес ntdll.dll
    • Проходимся по таблице экспорта ntdll, для поиска адресов функций LdrLoadDLL и LdrGetProcedureAddress.
    • Используем LdrLoadDll для загрузки gdiplus.dll, далее используем LdrGetProcedureAddress для получения нужных функций: GdiplusStartup, GdiplusShutdown, GdipCreateFromHWND, GdipCreatePen1, GdipSetPenStartCap, GdipSetPenEndCap, GdipSetPenDashArray, GdipDrawLineI, GdipDeletePen, GdipDeleteGraphics.
    • Все текстовые сообщения расшифровываем непосредственно перед использованием.
    • Все константы получаем после длительных вычислений.
    Вроде бы ничего не забыл...
     
  9. mantissa

    mantissa Мембер Команда форума

    Публикаций:
    0
    Регистрация:
    9 сен 2022
    Сообщения:
    167
    Цель таких уроков, все таки, по моему мнению - изучение ассемблера на примере API GDI+, у mikl цикл статей по созданию оконных приложений через асм, так там везде gdi используется, возможно он хочет там везде на gdi+ поменять в коде. Ну и + на асме много чего видно, чего нет в си, на примере какого-нибудь API приятнее учить синтаксис языка и тд, чем просто алгоритмические задачи решать условно.
     
  10. R81...

    R81... Active Member

    Публикаций:
    0
    Регистрация:
    1 фев 2020
    Сообщения:
    154
    Я тоже за Asm, хоть и не использую 64бит пока.
    Ясно, что пример проверен и работает.
    В файле:
    LineCapArrowAnchor = $14
    Не логичнее ли в программе:
    LineCapArrowAnchor = 14h
    Или я не понял чего?
     
  11. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    3.864
    R81..., это очепятка... конечно же должно быть LineCapArrowAnchor = 14h Хотя, стоп. Компилятор бы LineCapArrowAnchor = $14 не пропустил. А о каком файле идет речь? В коде самого верхнего поста LineCapArrowAnchor equ 14h в файле tut_01a.zip тоже все нормально...