Hex Viewer (GUI)

Я решил написать цикл статей на тему принципов создания простейшего HEX просмотрщика файлов. Не знаю точно, сколько будет статей в конечном итоге, поскольку сами статьи я пишу параллельно написанию кода. И да, не нужно ждать, что в статьях будет сделан упор на широкую функциональность или дизайн. Наш просмотрщик будет способен только открывать и читать файлы любых размеров, ну и конечно отображать их в нашем окне.

Начнем :)

Давайте возьмем программу "Блокнот" и попробуем туда загрузить файлик ну скажем размером 100 МБ. Попробовали? Сколько времени прошло, пока файл загрузился? Наверное успели налить чайку и даже больше. А теперь возьмем любой популярный HEX редактор и загрузим в него файл размером 1-2 ГБ (~1000-2000 МБ). Не успели даже моргнуть, а файл загружен. При чем, размер файла может быть на порядок большим. Как же так? Все дело в том, что текстовые редакторы (типа "Блокнот") грузят полностью все данные из файла в память. В принципе, для текстовых редакторов, функция которых лежит в редактировании небольших файлов, данный метод вполне подходит. Как же происходит загрузка огромных файлов в бинарные редакторы? В данных программах используется метод чтения выборочного небольшого участка памяти , которого с гарантией хватает для просмотра в окне редактора. Само же чтение файлов чаще всего осуществляется при помощи таких инструментов как Memory Mapped файлы. Сами по себе мэп-файлы (Memory Mapped файлы) ,при проецировании файлов в память, предоставляют указатель на любой участок загружаемого файла\участка файла, точно так же как указатель на любой адрес памяти. То есть мы с файлом работаем практически так же, как с памятью. При работе с мэп-файлами, нам необходимо проецировать файл в память при помощи функций (CreateFile и CreateFileMapping). А далее все что нам нужно, это вовремя получать указатель на нужный нам сегмент проекции памяти при помощи функции MapViewOfFile, и дальше читать данные относительно этого указателя, в рамках этого сегмента. Если нужны данные вне рамок данного сегмента, просто освобождаем указатель на старый сегмент при помощи UnmapViewOfFile и получаем новый. Мэп-файлы со своим простым и произвольным доступом к любому участку файла, позволяет быстро и легко перемещаться по файлу и читать. Именно эти функции я и буду использовать в HEX просмотрщике.


И так, что же нам будет нужно для написания простого просмотрщика файлов в виде HEX значений:

1) Создать главное окно
2) Создать свой класс дочернего окна, которое будет рисовать данные
3) Написать модуль загрузки данных посредством Memory Mapped файлов

Приступим!

Вот код создания главного окна:

#INCLUDE "windows.bi"
#INCLUDE "win/commctrl.bi"
InitCommonControls()

' Переменные
Dim Shared hinst As HINSTANCE : hinst = GetModuleHandle(0)
Dim msg As MSG
Dim As WNDCLASSEX wc
Dim As ZString*20 sNameClass = "HexViewer"
Dim Shared As HFONT hFontCourierNew
Dim Shared As HWND mainHwnd

' загрузка шрифта
Function LoadFontProc(Byval Name_     As String, _
    Byval Size      As Integer, _
    Byval corner    As Integer,_
    Byval BOLD      As Integer,_
    Byval Italic    As Integer,_
    Byval Underline As Integer,_
    Byval StrikeOut As Integer,_
    Byval CharSet As Integer) As HFONT
    Dim As Integer size_
    If BOLD=1 Then
        BOLD=700 ' жирный шрифт
    Else
        BOLD=400 ' обычный
    Endif
    Dim As HDC hdc = CreateDC("DISPLAY",0,0,0) ' создаем контекст
    size_ = -MulDiv(Size, GetDeviceCaps(hdc, LOGPIXELSY), 72) ' высчитываем размер шрифта
    DeleteDC(hdc) ' удаляем контекст
    Return CreateFont(size_,0,corner*10,0,Bold,Italic,Underline,StrikeOut, _ 'создаем шрифт
    CharSet, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH Or FF_DONTCARE,Name_)
End Function

' оконная процедура главного окна
Function wndproc(hwnd As HWND, msg As Uinteger,_
    wparam As WPARAM, lparam As LPARAM) As Integer

    Select Case msg
        Case WM_CREATE

        Case WM_COMMAND
            If lparam <> 0 Then ' если сообщения от контролов
                Select Case Loword(wParam)
                    Case 1
                    Case 2
                End Select
            Endif
        Case WM_DESTROY
            DeleteObject(hFontCourierNew)
            PostQuitMessage(0)' выходим
    End Select
    Return DefWindowProc(hwnd,msg,wparam,lparam)
End Function

' настройки главного окна
With wc
    .cbSize=SizeOf(WNDCLASSEX)
    .style=CS_HREDRAW Or CS_VREDRAW
    .lpfnWndProc=@wndproc
    .hInstance=Hinst
    .hIcon=LoadIcon(0,IDI_QUESTION)
    .hCursor=LoadCursor(0,IDC_ARROW)
    .hbrBackground=Cast(HBRUSH,COLOR_WINDOW)
    .lpszClassName=StrPtr(sNameClass)
    .hIconSm=.hIcon
End With

' регистрация окна
If RegisterClassEx(@wc)=0 Then
    Print "Register error, press any key"
    Sleep
    End
Endif

' Загрузка шрифтов
hFontCourierNew = LoadFontProc("Courier new",11,0,0,0,0,0,DEFAULT_CHARSET)

' создаем главное окно
mainHwnd = CreateWindowEx(0,sNameClass,"HexViewer",_
WS_VISIBLE Or WS_OVERLAPPED Or WS_CAPTION Or WS_SYSMENU Or WS_MINIMIZEBOX,100,100,800,610,0,0,Hinst,0)

' главный цикл
While GetMessage(@msg,0,0,0)
        TranslateMessage(@msg)
        DispatchMessage(@msg)
Wend


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

Теперь добавим так же код создания дочернего окна, в котором будем рисовать данные. Пока без всяких отрисовок, просто свое окошко , со своим названным классом. Вызывать данную процедуру будем из события WM_CREATE.

' создание HEX окна
Sub CreateChildProc(hwnd As hwnd)
    Dim As WNDCLASSEX wc
    With wc
        .cbSize=SizeOf(WNDCLASSEX)
        .lpfnWndProc=@HexWndProc
        .hInstance=Hinst
        .hCursor=LoadCursor(0,IDC_IBEAM)
        .hbrBackground=Cast(HBRUSH,COLOR_SCROLLBAR)
        .lpszClassName=StrPtr(szHexClass)
    End With

    If RegisterClassEx(@wc)=0 Then
        Print "Register error, press any key"
        Sleep
        End
    Endif
    childHwnd = CreateWindowEx( WS_EX_CLIENTEDGE,szHexClass,"",_
    WS_VSCROLL Or WS_CHILD Or WS_VISIBLE,0,25,794,552,hwnd,Cast(HMENU,1),Hinst,0)
End Sub


Так же напишем каркас оконной процедуры для данного окна:

' оконная процедура своего окна, на котором будем рисовать HEX данные
Function HexWndProc(hwnd As HWND, msg As Uinteger,_
    wparam As WPARAM, lparam As LPARAM) As Integer

    Select Case msg
        Case WM_PAINT ' если требуется рисование
                'Return 0   
        Case WM_VSCROLL ' если произошло событие скроллинга
    End Select
    Return DefWindowProc(hwnd,msg,wparam,lparam)

End Function

Ну и еще добавим кнопочку, для выбора загрузки файла. Лучше конечно тулбар, но зачем нам сейчас лишний код?
CreateWindowEx(0,"Button","Открыть файл",_
            WS_VISIBLE Or WS_CHILD,10,1,100,20,hwnd,Cast(HMENU,2),Hinst,0) ' Создаем обычную кнопку

Ну и чтобы в будущем не отвлекаться , сразу напишем диалог получения имени файла из проводника:
Dim Shared ofn  As OPENFILENAME
Dim Shared filename        As Zstring * 512

'....

' диалог получения имени файла
Function FileOpen(Byval Title As String,Byval curentdir As String, Byval Pattern As String, Byval templateName As String = "") As String 
  Dim fbguiTemp As Integer
  filename = templateName

  With ofn
    .hwndOwner       = 0
    .lStructSize     = Sizeof(OPENFILENAME)
    .lpstrFilter     = Cast(LPCTSTR,Strptr(Pattern) )
    .lpstrFile       = Strptr(filename)
    .nFileOffset     = 0
    .nMaxFile        = Sizeof(filename)
    .lpstrFileTitle  = Cast(LPTSTR,Strptr(Title))
    .nMaxFileTitle   = Sizeof(Title)
    .lpstrInitialDir = Cast(LPCTSTR,Strptr(curentdir))
    .lpstrTitle      = Cast(LPCTSTR,Strptr(Title))
    .Flags           = OFN_EXPLORER Or OFN_FILEMUSTEXIST
  End With

  If( GetOpenFileName( @ofn ) = 0 ) Then
    Return ""
  Else
    Return filename
  End If
End Function


На самом деле , большую часть кода мне и писать то не пришлось, я банально вытаскивал куски ранее написанного мною кода и немного правил под данный проект. Например загрузку шрифта, диалог выбора файлов я выдернул из исходников моей библиотеки, только чуть "причесал". А например код создания WINAPI окна у меня всегда располагается в шаблонах редактора FBEDIT.

Вот как-то так получилось все вместе:

#INCLUDE "windows.bi"
#INCLUDE "win/commctrl.bi"
#INCLUDE "win/commdlg.bi"
InitCommonControls()

Declare Function HexWndProc(hwnd As HWND, msg As Uinteger,_
wparam As WPARAM, lparam As LPARAM) As Integer

' Переменные
Dim Shared hinst As HINSTANCE : hinst = GetModuleHandle(0)
Dim msg As MSG
Dim As WNDCLASSEX wc
Dim As ZString*20 sNameClass = "HexViewer"
Dim Shared As ZString*20 szHexClass: szHexClass = "szHexClass"
Dim Shared As HFONT hFontCourierNew
Dim Shared As HWND mainHwnd
Dim Shared As HWND childHwnd
Dim Shared ofn  As OPENFILENAME
Dim Shared filename        As Zstring * 512

' загрузка шрифта
Function LoadFontProc(Byval Name_     As String, _
    Byval Size      As Integer, _
    Byval corner    As Integer,_
    Byval BOLD      As Integer,_
    Byval Italic    As Integer,_
    Byval Underline As Integer,_
    Byval StrikeOut As Integer,_
    Byval CharSet As Integer) As HFONT
    Dim As Integer size_
    If BOLD=1 Then
        BOLD=700 ' жирный шрифт
    Else
        BOLD=400 ' обычный
    Endif
    Dim As HDC hdc = CreateDC("DISPLAY",0,0,0) ' создаем контекст
    size_ = -MulDiv(Size, GetDeviceCaps(hdc, LOGPIXELSY), 72) ' высчитываем размер шрифта
    DeleteDC(hdc) ' удаляем контекст
    Return CreateFont(size_,0,corner*10,0,Bold,Italic,Underline,StrikeOut, _ 'создаем шрифт
    CharSet, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH Or FF_DONTCARE,Name_)
End Function

' диалог получения имени файла
Function FileOpen(Byval Title As String,Byval curentdir As String, Byval Pattern As String, Byval templateName As String = "") As String 
  Dim fbguiTemp As Integer
  filename = templateName

  With ofn
    .hwndOwner       = 0
    .lStructSize     = Sizeof(OPENFILENAME)
    .lpstrFilter     = Cast(LPCTSTR,Strptr(Pattern) )
    .lpstrFile       = Strptr(filename)
    .nFileOffset     = 0
    .nMaxFile        = Sizeof(filename)
    .lpstrFileTitle  = Cast(LPTSTR,Strptr(Title))
    .nMaxFileTitle   = Sizeof(Title)
    .lpstrInitialDir = Cast(LPCTSTR,Strptr(curentdir))
    .lpstrTitle      = Cast(LPCTSTR,Strptr(Title))
    .Flags           = OFN_EXPLORER Or OFN_FILEMUSTEXIST
  End With

  If( GetOpenFileName( @ofn ) = 0 ) Then
    Return ""
  Else
    Return filename
  End If
End Function

' создание HEX окна
Sub CreateChildProc(hwnd As hwnd)
    Dim As WNDCLASSEX wc
    With wc
        .cbSize=SizeOf(WNDCLASSEX)
        .lpfnWndProc=@HexWndProc
        .hInstance=Hinst
        .hCursor=LoadCursor(0,IDC_IBEAM)
        .hbrBackground=Cast(HBRUSH,COLOR_SCROLLBAR)
        .lpszClassName=StrPtr(szHexClass)
    End With

    If RegisterClassEx(@wc)=0 Then
        Print "Register error, press any key"
        Sleep
        End
    Endif
    childHwnd = CreateWindowEx( WS_EX_CLIENTEDGE,szHexClass,"",_
    WS_VSCROLL Or WS_CHILD Or WS_VISIBLE,0,25,794,552,hwnd,Cast(HMENU,1),Hinst,0)
End Sub

' оконная процедура главного окна
Function wndproc(hwnd As HWND, msg As Uinteger,_
    wparam As WPARAM, lparam As LPARAM) As Integer

    Select Case msg
        Case WM_CREATE
            
            CreateChildProc(hwnd) ' Создаем свое дочернее окно для вывода HEX значений
            
            CreateWindowEx(0,"Button","Открыть файл",_
            WS_VISIBLE Or WS_CHILD,10,1,100,20,hwnd,Cast(HMENU,2),Hinst,0) ' Создаем обычную кнопку
            
        Case WM_COMMAND
            If lparam <> 0 Then ' если сообщения от контролов
                Select Case Loword(wParam)
                    Case 2
                        ? FileOpen("Открыть","C:\","All files (*.*)"+Chr(0)+"*.*"+Chr(0) ,"")
                End Select
            Endif
        Case WM_DESTROY
            DeleteObject(hFontCourierNew)
            PostQuitMessage(0)' выходим
    End Select
    Return DefWindowProc(hwnd,msg,wparam,lparam)
End Function

' оконная процедура своего окна, на котором будем рисовать HEX данные
Function HexWndProc(hwnd As HWND, msg As Uinteger,_
    wparam As WPARAM, lparam As LPARAM) As Integer

    Select Case msg
        Case WM_PAINT ' если требуется рисование
                'Return 0   
        Case WM_VSCROLL ' если произошло событие скроллинга
    End Select
    Return DefWindowProc(hwnd,msg,wparam,lparam)

End Function

' настройки главного окна
With wc
    .cbSize=SizeOf(WNDCLASSEX)
    .style=CS_HREDRAW Or CS_VREDRAW
    .lpfnWndProc=@wndproc
    .hInstance=Hinst
    .hIcon=LoadIcon(0,IDI_QUESTION)
    .hCursor=LoadCursor(0,IDC_ARROW)
    .hbrBackground=Cast(HBRUSH,COLOR_WINDOW)
    .lpszClassName=StrPtr(sNameClass)
    .hIconSm=.hIcon
End With

' регистрация окна
If RegisterClassEx(@wc)=0 Then
    Print "Register error, press any key"
    Sleep
    End
Endif

' Загрузка шрифтов
hFontCourierNew = LoadFontProc("Courier new",11,0,0,0,0,0,DEFAULT_CHARSET)

' создаем главное окно
mainHwnd = CreateWindowEx(0,sNameClass,"HexViewer",_
WS_VISIBLE Or WS_OVERLAPPED Or WS_CAPTION Or WS_SYSMENU Or WS_MINIMIZEBOX,100,100,800,610,0,0,Hinst,0)

' главный цикл
While GetMessage(@msg,0,0,0)
    TranslateMessage(@msg)
    DispatchMessage(@msg)
Wend


Это пока все , что касается GUI . Если в голову взбредет еще что-то, напишу позже. В следующей статье я рассчитываю написать о рисовании в окне HEX Viewer.

 

содержание | вперед