|
|
Пишем текстовый редактор 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 мгновенно справился!!
О, Асм! Ты - мир!!:)
|