Assembler
.NET
Delphi
Windows
Reversing&Cracking
Шаолинь
Other
Форум Monah'а

Пишем текстовый редактор I

Автор: Adrax

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

Эта идея появилась у меня, когда я увидел в примерах к FASM программку MINIPAD. Я решил сделать из него более-менее приличный текстовый редактор, не уступающий стандартному Notepad. Параллельно со мной этой темой увлёкся другой член нашей команды - Mohah - и добился в этом гораздо больших успехов, чем я, т.к. у меня было очень мало свободного времени (сессия). Но сейчас ситуация диаметрально противоположна: Monah занят, а я свободен - поэтому писать статью про текстовый редактор выпало мне:) Но я всё-таки надеюсь, что и Monah порадует нас своими умозаключениями и примерами кода

Итак, чтобы выйти на уровень Блокнота, MINIPAD'у не хватает самой малости: умения открывать, закрывать и печатать файлы, плюс вести поиск в тексте. Я решил, взяв за основу MINIPAD, написать свой plain-text editor, обладающий всеми возможностями Блокнота. Назвать я его решил RFpad:)

Я хочу от всей души поблагодарить leo, q_q, JAPH с форума WASM.RU за их поистине ангельское терпение и готовность помочь толковым советом бестолковому программисту

Файлы

Итак, давайте начнём с азов: с открытия/сохранения файлов. Там всё довольно логично и просто. Нужно рассмотреть три ипостаси файла: файл на диске, файл в памяти, файл в окне открывшего его приложения
Чтение/запись файла на диске производятся уже знакомыми вам функциями ReadFile/WriteFile, так что, думаю, с этим проблем не возникнет
Вот с памятью всё чуток позапутаннее. Сначала нужно выделить необходимый объём памяти из кучи (heap - динамической памяти) функцией GlobalAlloc:
HGLOBAL GlobalAlloc(
UINT uFlags,//флаги
DWORD dwBytes//сколько байт выделять
);

Флаги у этой функи такие: GMEM_FIXED - выделить фиксированный блок памяти, GMEM_MOVEABLE - выделить перемещаемый (внутри владеющей ими кучи) блок памяти, GMEM_ZEROINIT - заполнить выделенный блок нулями, GPTR - комбинация 1 и 3, GHND - комбинация 2 и 3
В случае облажания функция возвращает 0 в eax, в случае успешного выполнения - варьируется в зависимости от флагов: если юзались GMEM_FIXED или GPTR (но их почти нигде не используют) - то указатель на выделенную память, в прочих случаях - её хэндл. Тогда необходимо по хэндлу получить указатель - это делает GlobalLock

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

Совершив и это злодеяние, избавимся от указателя на память (GlobalUnlock) и её хэндла (GlobalFree)

Ладно, с внутренними механизмами разобрались, пора подумать и о междумордии (интерфейсе). В качестве такового у нас будут выступать стандартные диалоги открытия/сохранения файла. Для создания таких диалогов используются функции GetOpenFileName и GetSaveFileName соответственно. В качестве параметра и та, и другая требуют указатель на структуру OPENFILENAME:

typedef struct tagOFN {
DWORD lStructSize;//размер в байтах (76 байт)
HWND hwndOwner;//хэндл окна-владельца
HINSTANCE hInstance;//хэндл процесса
LPCTSTR lpstrFilter;//указатель на фильтры
LPTSTR lpstrCustomFilter;
DWORD nMaxCustFilter;
DWORD nFilterIndex;//номер основного фильтра
LPTSTR lpstrFile;//указатель на буфер для пути и имени файла
DWORD nMaxFile;//размер буфера (минимум 260 байт)
LPTSTR lpstrFileTitle;
DWORD nMaxFileTitle;
LPCTSTR lpstrInitialDir;
LPCTSTR lpstrTitle;//указатель на заголовок для диалога
DWORD Flags;//флаги
WORD nFileOffset;//смещение первого символа собственно имени файла в буфере
WORD nFileExtension;//смещение первого символа расширения файла в буфере
LPCTSTR lpstrDefExt;
DWORD lCustData;
LPOFNHOOKPROC lpfnHook;
LPCTSTR lpTemplateName;
} OPENFILENAME;

Дам пояснение некоторым моментам. По поводу фильтров: вы, наверняка, помните юзаемый в этих диалогах список типов файлов - его как раз и задают строки фильтров. Сколько типов файлов предлагает открыть/сохранить диалог - столько и нужно фильтров. Фильтр задаётся так: тип файла,0,маска,0, в конце списка фильтров ставится ещё один, дополнительный 0. Фильтры нумеруются, начиная с 1, в порядке объявления. По умолчанию подставляется тот фильтр, номер которого занесён в поле nFilterIndex
Флагов куча... По ходу исходника я буду комментировать используемые

Вызывая GetOpenFileName или GetSaveFileName, мы создаём диалог и получаем имя открываемого/сохраняемого файла, чтобы потом извращаться над ним: GlobalAlloc, GlobalLock, ReadFile/WriteFile, GlobalUnlock, GlobalFree... Непонятно? Ничего страшного - я сам долго въехать не мог:) Поверьте, в коде это всё гораздо очевиднее

И ещё... Мы должны рассмотреть гипотетическую возможность того, что юзер вызовет программу из командной строки вместе с именем файла. Или вообще - поставит RFpad текстовым редактором по умолчанию:) В общем, необходимо при запуске анализировать командную строку. Для этого существует функция GetCommandLine, вызываемая без параметров и возвращающая указатель на командную строку, которую необходимо будет пропарсить

Ресурсы

В написании секции .rsrc существует два подхода. Первый - описывать ресурсы прямо в исходнике программы. Второй - описывать их в отдельном .rc-скрипте, на Си-подобном языке с последующей компиляцией в .res-файл и прилинковкой к основному модулю. Не поверите, но второй подход по ряду причин гораздо проще первого! Сейчас я вам покажу, как выглядит .rc для нашей сегодняшней программы:

//определяем ID'ы пунктов меню
#define IDM_NEW             0x100L
#define IDM_OPEN            0x101L
#define IDM_SAVE            0x102L
#define IDM_SAVEAS          0x103L
#define IDM_EXIT            0x104L
#define IDM_ABOUT           0x105L
#define IDM_UNDO            0x106L
#define IDM_CUT             0x107L
#define IDM_COPY            0x108L
#define IDM_PASTE           0x109L
#define IDM_CLEAR           0x10AL
#define IDM_SETSEL          0x10BL

//ID'ы основных ресурсов
#define ID_MENU             0x700L
#define ID_ACCEL            0x701L
#define ID_ABOUT            0x702L
#define ID_ICON             0x703L

//ресурсы задаются так:
//ID ресурса, затем ключевое слово, затем описание
//например, IKONKA ICON "1.ico"
//или KARTINKA BITMAP "xxx.bmp"
//очень часто после этого пишется служебное слово DISCARDABLE

//иконка
ID_ICON ICON "1.ico"
//естественно, что на момент компиляции
//файл 1.ico должен лежать в той же папке,
//где исходник и .rc-скрипт

//описываем меню, состоящее из трёх списков: File, Edit, About
//меню потому кажется сложным, что приходится описывать
//каждый список (POPUP), а в списках -
//пункты меню (MENUITEM)
//меню в целом и каждый список надо брать в фигурные скобки
//в описании каждого пункта меню идёт сначала отображаемый в нём текст,
//затем его ID
ID_MENU MENU DISCARDABLE {
POPUP "&File" {
 MENUITEM "&New\tCtrl+N", IDM_NEW
 MENUITEM "&Open...\tCtrl+O", IDM_OPEN
 MENUITEM "&Save\tCtrl+S", IDM_SAVE
 MENUITEM "Save &As...\tCtrl+Shift+S", IDM_SAVEAS
 MENUITEM SEPARATOR
 MENUITEM "E&xit\tCtrl+Q", IDM_EXIT
}
POPUP "&Edit" {
 MENUITEM "&Undo\tCtrl-Z", IDM_UNDO
 MENUITEM SEPARATOR
 MENUITEM "Cu&t\tCtrl-X", IDM_CUT
 MENUITEM "&Copy\tCtrl-C", IDM_COPY
 MENUITEM "&Paste\tCtrl-V", IDM_PASTE
 MENUITEM "&Delete\tDel", IDM_CLEAR
 MENUITEM SEPARATOR
 MENUITEM "Select &All\tCtrl-A", IDM_SETSEL
}
POPUP "&Help" {
 MENUITEM "About", IDM_ABOUT
}}

//таблица акселераторов
//описываем акселераторы, сопосталяя им ID'ы аналогичных пунктов меню
//акселераторы - это горячие клавиши
//каждый акселератор описывается так:
//сначала буква, изображённая на этой горячей клавише,
//затем указание, как именно её следует нажимать,
//чтобы сработал акселератор
ID_ACCEL ACCELERATORS DISCARDABLE {
"N", IDM_NEW, CONTROL, VIRTKEY//нажать саму клавишу (virtkey) и одновременно Ctrl
"O", IDM_OPEN, CONTROL, VIRTKEY
"S", IDM_SAVE, CONTROL, VIRTKEY
"S", IDM_SAVEAS, CONTROL, SHIFT, VIRTKEY//клавишу+Ctrl+Shift
"Q", IDM_EXIT, CONTROL, VIRTKEY
"Z", IDM_UNDO, CONTROL, VIRTKEY
"A", IDM_SETSEL, CONTROL, VIRTKEY
}

Вот так должно выглядеть окно нашей программы:

Как видите, присутствуют три списка меню: File, Edit и About, а также саморисованная иконка. В описании ресурса в качестве ID'а можно использовать любое символьное имя, главное - чтобы имена в .rc и .asm совпадали. Странным числам не удивляйтесь: просто так задаются шестнадцатиричные числа на Си. 0x700L это то же самое, что и dd 700h
Немаловажная тонкость: IDM'ы (ID'ы пунктов меню) должны быть расположены в .rc в том же порядке, что и их обработчики в .asm
В описании пунктов меню используется "\t" - это тире. В знаке "&" тоже нет ничего загадочного: буква после этого знака отображается подчёркнутой (а нажатие Alt+эта буква симулирует выбор этого пункта меню)
Надеюсь, вы смогли разобраться в структуре .rc-скрипта. Он довольно-таки просто устроен

Сохранив этот файл как RFpad.rc, я скомпилировал его rc.exe из комплекта MASM32 (у вас пути к файлам, скорее всего, другие):

C:\Documents and Settings\Adrax>e:
E:\>cd masm32\bin
E:\masm32\bin>rc e:\zlo\fasm\projects\rfpad.rc

Также для компиляции .rc-скриптов может использоваться, например, brcc32.exe из поставки Borland Delphi
После компиляции .rc-скрипта получится бинарник с расширением .res

И иконку, и таблицу акселераторов в приложении необходимо будет подгрузить из ресурсов, используя LoadIcon и LoadAccelerators соответственно. Эти функции принимают первым параметром хэндл приложения, вторым - ID ресурса

Обработка сообщений

Это, без сомнения, самая сложная и объёмная часть работы... Посмотрим, какие именно сообщения нам придётся обрабатывать:
  • WM_CREATE - чтобы приложение смогло создать поле для ввода текста. Я делаю всё один-в-один с MINIPAD - также юзаю с этой целью Edit и создаю его как дочернее окно
  • WM_ACTIVATE - если юзер щёлкнет по нашему окну мышкой, оно обязано стать активным и получить фокус ввода
  • WM_SIZE - юзер сможет, ухватившись мышью, менять размер окна
  • WM_DESTROY и WM_CLOSE - чтобы окно можно было закрыть
  • WM_COMMAND...
Обрабатывать WM_COMMAND нужно, чтобы реагировать на тыкание в элементы управления. В прошлой статье мы уже касались этого вопроса и обрабатывали сообщения от контролов (конкретно - от кнопок). Наше сегодняшнее приложение кнопок в окне не создаёт, поэтому нам придётся обрабатывать только сообщения от пунктов меню

Сообщения WM_COMMAND от контролов и от пунктов меню различаются по своему содержимому. Если WM_COMMAND пришло от контрола, то в lParam у него будет хэндл контрола, в верхнем слове wParam - код уведомления, в нижнем слове wParam - ID контрола
WM_COMMAND, пришедшее от пункта меню, содержит в нижнем слове wParam ID меню, всё остальное забито нулями

Поэтому обработка WM_COMMAND будет выглядеть следующим образом:

  • Сначала сравниваем lParam с нулём. Если равно - сообщение пришло от меню, если нет - от контрола. Так как наше сегодняшнее приложение контролов не имеет, то сообщения с ненулевым lParam мы вправе игнорировать
  • Затем копируем wParam в eax, и в ax (младшей половине регистра) у нас будет ID пункта меню (IDM)
  • Мы заранее определили IDM, на которые будем реагировать (посмотрите в .rc-скрипте: от IDM_NEW до IDM_SETSEL, от 100h до 10Bh), поэтому необходимо убедиться, что полученное нами сообщение принадлежит к числу обрабатываемых
  • Передаём управление на обработчик сообщения. На каждый IDM - свой обработчик
Как же передать управление на нужный обработчик? Взгляните в исходник MINIPAD и в оконной процедуре увидите вот такую конструкцию:
cmp [wmsg],WM_CREATE
je wmcreate
cmp [wmsg],WM_SIZE
je wmsize
cmp [wmsg],WM_SETFOCUS
je wmsetfocus
cmp [wmsg],WM_COMMAND
je wmcommand
cmp [wmsg],WM_DESTROY
je wmdestroy
Сравнение-прыжок, сравнение-прыжок... В языках программирования высокого уровня аналогом такой конструкции будет оператор множественного выбора case of, известный своей тормознутостью:) Сами подумайте: если получено сообщение WM_COMMAND, то, прежде чем управление получит его обработчик, должно пройти три сравнения! Учтите ещё, что процессоры очень не любят ветвления кода, и потому такая хрень будет отрицательно сказываться на производительности

Я и сам заюзал в коде подобную конструкцию, но слегка схитрил: сравнение с WM_COMMAND сделал самым первым (всё-таки, приложение получает это сообщение чаще всего), следующим - WM_ACTIVATE, затем - WM_SIZE, ну а однократно вызываемые WM_CLOSE, WM_CREATE, WM_DESTROY - в самый конец
Но вот при обработке WM_COMMAND мне захотелось применить более прогрессивный метод:

  • Создадим таблицу указателей на обработчики различных IDM (главное - чтобы указатели перечислялись в том же порядке, что и IDM в .rc-скрипте)
  • Вычтем из ax (как помните, туда мы кладём нижнее слово wParam) наименьший обрабатываемый IDM - получим индекс в таблице
  • Вызовем обработчик, передав управление по указателю с полученным индексом
Не следует забывать и об обработке сообщений от горячих клавиш (акселераторов). В .rc-скрипте мы поставили в соответствие каждому акселератору определённый IDM. Чтобы преобразовать сообщение от акселератора в WM_COMMAND с этим IDM в wParam, нужно вызывать функцию TranslateAccelerator в цикле обработки сообщений, передав ей хэндл окна, хэндл таблицы акселераторов и указатель на структуру сообщения
Я с этой функцией мучался очень долго, пока на форуме не подсказали: полученный с помощью TranslateAccelerator IDM необходимо подвергнуть операции and с числом 0FFFFh - тогда получится нормальный IDM, который можно обрабатывать по вышеописаному методу

И ещё одна тонкость, о которой я и не подозревал, пока меня не ткнули в неё носом q_q, leo и JAPH: внутри оконной процедуры можно использовать только одну команду ret - в конце процедуры. Все прочие ret внутри неё должны быть заменены на retn или её опкод db 0C3h, иначе они неправильно ассемблируются, и программа не будет работать

Собственно кодинг

Итак, основные тонкости мы подробно рассмотрели, теперь приступим к написанию ассемблерного кода. Если что-то осталось для вас непонятным - внимательно читайте комментарии. Если же что-то окончательно повергло вас в уныние - пишите мне, постараюсь помочь:)

format PE GUI 4.0
include 'win32axp.inc'

;определяем ID'ы основных ресурсов
;я обозвал их так,
;вы вправе дать им любые символьные имена
;главное - чтобы эти имена
;и их числовые значения совпадали
;в .asm и .rc файлах
ID_MENU = 700h
ID_ACCEL = 701h
ID_ABOUT = 702h
ID_ICON = 703h

.data

name db 'RFpad',0;RFpad будет и именем класса окна, и его заголовком
editK db 'edit',0;класс edit'а
changes db 'Do you want to save changes?',0
;эту фразу будем выводить в MessageBox'е

filter  db  "All Files",0,'*.*',0
        db  "Text Files",0, '*.txt',0,0
;фильтры

wc WNDCLASS 0,Procedura,0,0,NULL,NULL,NULL,COLOR_BTNFACE+1,ID_MENU,name
;частично заполнили оконную структуру

textt db '.txt',0
;расширение, которое будем подставлять при сохранении

ofn OPENFILENAME sizeof.OPENFILENAME,0,0,filter,0,0,2,buffer, 260,0,0,0,0,0,0,0,0,0,0,0
;определяем структуру ofn типа OPENFILENAME
;и частично заполняем:
;передаём ей указатель на буфер, его размер
;также передаём указатель на фильтры
;основным фильтром будет второй - "Text files"
;мне было лень писать размер структуры в 76 байт,
;поэтому я заставил FASM вычислить его самостоятельно,
;с помощью макроса sizeof:)

buffer rb 260;260-байтный буфер под путь и имя файла

newflag db 1;вспомогательная переменная
;если она равна 1 - значит, файл заново создан,
;если 0 - значит, открыт ранее созданный файл

edith dd ?;хэндл edit'а
pmem dd ?;указатель на выделенную память
accelh dd ?;хэндл таблицы акселераторов
sizeRW dd ?;вспомогательная переменная - сколько байт прочитано/записано
hmem dd ?;хэндл выделенной памяти
filesize dd ?;переменная, хранящая размер файла
client RECT;клиентская область
msg MSG;структура сообщения

;таблица указателей на обработчики
table dd NEW
      dd OPEN
      dd SAVE
      dd SAVEAS
      dd EXIT
      dd ABOUT
      dd UNDO
      dd CUT
      dd COPY
      dd PASTE
      dd CLEAR
      dd SETSEL

.code

fuck:
 invoke GetCommandLine;получаем указатель на командную строку
 mov edi,eax;кладём его в eax
 mov al,20h;искать будем пробел (20h)
 mov ecx,260;максимальную длину командной строки определяем в 260 символов
 repne scasb;ищем пробел: "сравниваем символы, пока не равно"
;теперь edi содержит указатель на первый символ после пробела
 cmp byte [edi],0;не ноль ли это? (конец строки, т.е. вызов без параметров)
 je cmdline_empty;если да - прыгаем
 mov esi,edi;сохраняем указатель на первый символ имени в esi
 mov edi,buffer;в edi - указатель на буфер
 rep movsb;копируем имя файла в буфер
 mov [newflag],0;а флаг обнуляем

cmdline_empty:
 invoke GetModuleHandle,0;получаем хэндл приложения
 mov [wc.hInstance],eax
 invoke LoadIcon,[wc.hInstance],ID_ICON;грузим иконку из ресурсов
 mov [wc.hIcon],eax
 invoke LoadCursor,0,IDC_ARROW;а курсор будем юзать стандартный
 mov [wc.hCursor],eax
 invoke RegisterClass,wc
 
invoke CreateWindowEx,0,name,name, WS_VISIBLE+WS_OVERLAPPEDWINDOW, 128,128, 192,192, NULL,NULL,[wc.hInstance],NULL
 mov esi,eax;заносим хэндл окна в esi
 invoke LoadAccelerators,[wc.hInstance],ID_ACCEL
 ;получаем хэндл на таблицу акселераторов
 mov [accelh],eax


;цикл обработки сообщений
msg_loop:
 invoke GetMessage,msg,NULL,0,0
 test eax,eax;проверка на WM_QUIT
 jz end_loop

;вот так выглядит транслирование сообщений
;от акселераторов
 invoke TranslateAccelerator,esi,[accelh],msg
 test eax,eax
 jnz msg_loop

 invoke  TranslateMessage,msg
 invoke  DispatchMessage,msg
 jmp     msg_loop
end_loop:
 invoke  ExitProcess,[msg.wParam]



proc Procedura hwnd,wmsg,wparam,lparam
 push ebx
 push esi
 push edi
 cmp [wmsg],WM_COMMAND
 je Command
 cmp [wmsg],WM_ACTIVATE
 je Act
 cmp [wmsg],WM_SIZE
 je Siz
 cmp [wmsg],WM_CLOSE
 je Close
 cmp [wmsg],WM_CREATE
 je Create
 cmp [wmsg],WM_DESTROY
 je Destroy

xx:
 invoke  DefWindowProc,[hwnd],[wmsg],[wparam],[lparam]
 jmp finish

Close:
;закрываем окно
 call saving;вдруг, надо сохранить?
 jmp xx

Create:
;создаём Edit для ввода текста
 invoke GetClientRect,[hwnd],client;получаем клиентскую область
 
invoke CreateWindowEx, WS_EX_CLIENTEDGE,editK,0, WS_VISIBLE+ WS_CHILD+WS_HSCROLL +WS_VSCROLL+ES_AUTOHSCROLL +ES_AUTOVSCROLL+ES_MULTILINE, [client.left],[client.top],[client.right], [client.bottom],[hwnd],0,[wc.hInstance],NULL
 test eax,eax;прокатило ли создание Edit'а?
 jz fail;если нет - рапортуем об обломе
 mov [edith],eax;иначе сохраняем его хэндл
 invoke SetFocus,eax;и передаём ему фокус
 cmp [newflag],1;пуста ли командная строка?
 je continue;если да - всё в ажуре
 call getopen;если нет - займёмся геморроем

continue:
 xor eax,eax
 jmp finish

fail:
 or eax,-1
 jmp finish


Command:
 cmp [lparam],0
 jnz continue;от меню ли поступило сообщение? проверяем...
 mov eax,[wparam]
 ;в ax сейчас ID меню
 and eax,0FFFFh
 ;and нужно делать, чтобы акселераторы сработали
 sub ax,100h
 jb continue
 ;мы, определяя ID'ы дали им номера от 100h и выше
 ;соответственно ID'ы ниже 100h мы не рассматриваем
 ;выше 10Bh - тоже
 cmp ax,0Bh
 ja continue
;вызов нужного обработчика по смещению его метки в таблице
 call dword [table+eax*4]
 jmp continue

SETSEL:
 invoke SendMessage,[edith],EM_SETSEL,0,-1;выделить весь текст
 retn

CLEAR:
 mov eax,WM_CLEAR;стереть выделенную часть текста
 jmp sendt

PASTE:
 mov eax,WM_PASTE;вставить текст из Буфера обмена (Clipboard)
 jmp sendt

COPY:
 mov eax,WM_COPY;скопировать выделенный кусок текста в Clipboard
 jmp sendt

CUT:
 mov eax,WM_CUT;вырезать выделенный кусок текста в Clipboard
 jmp sendt

UNDO:
 mov eax,EM_UNDO;отменить предыдущее действие

sendt:
;в eax сейчас сообщение, помещённое туда обработчиком
;надо послать его Edit'у
 invoke SendMessage,[edith],eax,0,0
 retn

NEW:
 call saving;вдруг, надо сохранить?
 mov [newflag],1
 invoke SendMessage,[edith],WM_SETTEXT,0,0;посылаем Edit'у пустой WM_SETTEXT
 retn

ABOUT:
 invoke MessageBox,[hwnd],'RFpad text editor by Adrax','RFpad',MB_OK
 retn

OPEN:
 call saving;открывая новый файл, должны же мы закрыть старый?
 mov [ofn.Flags],OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_EXPLORER
;ну, с флагами, думаю всё понятно:
;открыть существующий файл по существующему пути,
;диалог открытия сделать в стиле Explorer'а
;можно ещё флаг OFN_NOCHANGEDIR добавить,
;чтобы каждый раз открытие файла начиналось
;с одной и той же папки
 invoke GetOpenFileName,ofn
 test eax,eax
 ;удалось открыть?
 ;если нет - прыгаем
 jz fileopenfailed

getopen:
;эта метка - логичное продолжение открытия файла из диалога
;или начало работы проги, если её вызвали с именем файла из командной строки
;так или иначе, но в buffer у нас путь и имя файла
 
invoke CreateFile,buffer, GENERIC_READ or GENERIC_WRITE, FILE_SHARE_READ or FILE_SHARE_WRITE,0, OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE,0;создаём объект типа "файл" с указанным именем
 mov edi,eax;в edi - хэндл файла
 invoke GetFileSize,edi,0;выясняем размер файла
 inc eax;увеличиваем его на 1
 ;с учётом завершающего нуля
 mov [filesize],eax
 invoke GlobalAlloc,GHND,eax;выделяем память из кучи,
 ;получаем её хэндл
 mov [hmem],eax
 invoke GlobalLock,eax;присваиваем эту память себе,
 ;получаем указатель на неё
 mov [pmem],eax
 invoke ReadFile,edi,eax,[filesize],sizeRW,0
 ;читаем файл в выделенную область памяти
 invoke SendMessage,[edith],WM_SETTEXT,0,[pmem];выводим в окно текст из файла
 ;WM_SETTEXT помещает в Edit весь переданный буфер до первого нулевого символа
 invoke GlobalUnlock,[pmem];отказываемся от выделенного блока памяти
 invoke GlobalFree,[hmem];возвращаем память куче
 invoke CloseHandle,edi;закрываем файл
 mov [newflag],0

fileopenfailed:
;в моём варианте,
;если файл открыть не удалось,
;то прога ничем не показывает
;своего разочарования:)
;вам ничего не мешает прикрутить сюда
;вызов MessageBox'а
 invoke SetFocus,[edith]
 retn

SAVE:
 cmp [newflag],1;если файл не новый - сохраняем на старое место
 jnz getsave

SAVEAS:
 mov [ofn.Flags],OFN_EXPLORER or OFN_OVERWRITEPROMPT
;диалог будет в стиле Explorer'а
;с запросом на перезапись существующего файла,
;если юзер выбрал то же имя
 invoke GetSaveFileName,ofn
 ;получаем имя,
 ;под которым файл предполагается сохранить
 cmp [ofn.nFilterIndex],2;пользователь выбрал "Text file"?
 jnz yy;если нет - прыгаем дальше
 ;если да - пришиваем к имени файла расширение
       push eax
       invoke lstrcat,buffer,textt
       pop eax
yy:
 test eax,eax
 ;проверяем успешность выбора имени для сохранения
 jz filesavefailed

getsave:
 
invoke CreateFile,buffer, GENERIC_READ or GENERIC_WRITE, FILE_SHARE_READ or FILE_SHARE_WRITE,0, CREATE_ALWAYS, FILE_ATTRIBUTE_ARCHIVE,0;создаём объект типа "Файл" с указанным именем
 mov edi,eax;хэндл файла положим в edi
 invoke SendMessage,[edith],WM_GETTEXTLENGTH,0,0
 ;узнали число символов текста, оно же - размер текстовика
 ;учтите, что WM_GETTEXTLENGTH не учитывает завершающий нуль,
 ;поэтому размер надо увеличить на 1
 inc eax
 mov [filesize],eax
 invoke GlobalAlloc,GHND,[filesize];выделили память под файл
 mov [hmem],eax
 invoke GlobalLock,eax;получили на неё указатель
 mov [pmem],eax
 invoke SendMessage,[edith],WM_GETTEXT,[filesize],eax
 ;копируем текст из окна в выделенную область памяти
 ;в eax - количество скопированных байт
 invoke WriteFile,edi,[pmem],eax,sizeRW,0;пишем файл на диск
 invoke GlobalUnlock,[pmem]
 invoke GlobalFree,[hmem]
 ;возвращаем память
 invoke CloseHandle,edi
 ;закрываем файл
 invoke SendMessage,[edith],EM_SETMODIFY,0,0;сбрасываем флаг модификации
 mov [newflag],0

filesavefailed:
;та же петрушка,
;что и с fileopenfailed
 invoke SetFocus,[edith]
 retn

EXIT:
 call saving;сохранить содержимое перед выходом?
 invoke DestroyWindow,[hwnd]
 retn

Siz:
;обрабатываем попытку юзера растянуть/уменьшить окно
 invoke GetClientRect,[hwnd],client
 
invoke MoveWindow,[edith],[client.left],[client.top], [client.right],[client.bottom],TRUE
;принимаем выбранный юзером размер
 xor eax,eax
 jmp finish

Act:
;в нас ткнули мышкой?
;просто берём фокус на себя
 invoke SetFocus,[edith]
 xor eax,eax
 jmp finish

saving:
 invoke SendMessage,[edith],EM_GETMODIFY,0,0;изменялся ли текст
 test eax,eax
 jz not_modified
    ;если да - спросим у юзера, надо ли его сохранять
    invoke MessageBox,[hwnd],changes,name,MB_YESNO + MB_ICONWARNING
    cmp eax,IDYES;нажал ли он "Yes"?
 jne not_modified;если нет - прыгаем на выход
        call SAVE;если да - сохраняем
not_modified:
        retn

Destroy:
 invoke  PostQuitMessage,0
 xor eax,eax


finish:
 pop edi
 pop esi
 pop ebx
 ret
endp

section '.rsrc' resource from 'RFpad.res' data  readable discardable
;секцию ресурсов построим из файла RFpad.res

.end fuck

Итак, помещаем в одну папку исходник, файл ресурсов PFpad.res и иконку 1.ico, командуем FASM'у компилировать - вуаля! текстовый редактор готов!!

Не спешите праздновать:) До Блокнота нашему детищу ещё далеко... Надо ещё, как минимум, прикрутить диалог печати и поиск по тексту, надписи все русифицировать. Кроме того, я никак не смог вместить в окно RFpad'а более 24737 символов (кстати, в MINIPAD тоже больше не влезает - видимо, таково ограничение Edit'а). Да и неоптимизирована программка нихрена. Мне, вот Monah хвастался, что его текстовый редактор открывает файлы в 300 Мб весом, даже не поморщившись. Хоть бы сказал, как он это сделал...

В общем, не прощаемся, дорогие читатели! В следующей статье мы постараемся сделать из нашей программки зверь-софтину, рвущую все аналоги в клочья:)

До встречи!!

Добавлено 07.07.2007

Огромное спасибо Y_Mur и q_q! Чтобы снять ограничение на 24737 символов, нужно в обработчике WM_CREATE после создания Edit'а послать ему сообщение EM_SETLIMITTEXT с параметрами, равными нулю, проще говоря invoke SendMessage,[edith],EM_SETLIMITTEXT,0,0
В Win9x это расширяет ограничения Edit'а до 65535 символов, а в NT - вообще снимает их! На радостях я запузырил в RFpad 31 миллион символов сплошным блоком и сохранил в текстовик. И знаете что самое радостное? Notepad при попытке его открытия нахрен завис, а RFpad мгновенно справился!!

О, Асм! Ты - мир!!:)

Главная страница Windows Delphi Assembler .NET Delphi Reversing Шаолинь Other Форум Monah'а

Создатель команды, главный редактор, художник и web-мастер: Adrax

Дизайн сайта: WargaL

Ответственный за форум: Monah

RussianFuckersTeam©