fpc/docs/gtk4.tex
2001-07-10 21:52:18 +00:00

1499 lines
58 KiB
TeX

\documentclass[10pt]{article}
\usepackage{a4}
\usepackage{epsfig}
\usepackage{listings}
\usepackage{tabularx}
\lstset{language=Delphi}%
\lstset{basicstyle=\sffamily\small}%
\lstset{commentstyle=\itshape}%
\lstset{keywordstyle=\bfseries}%
\lstset{blankstring=true}%
\newcommand{\file}[1]{\textsf{#1}}
\usepackage[pdftex]{hyperref}
\newif\ifpdf
\ifx\pdfoutput\undefined
\pdffalse
\else
\pdfoutput=1
\pdftrue
\fi
\begin{document}
\title{Programming GTK in Free Pascal: Making a real-world application.}
\author{Florian Kl\"ampfl\\and\\Micha\"el Van Canneyt}
\date{January 2001}
\maketitle
\section{Introduction}
In the third article on programming the GTK toolkit, the use of several
GTK widgets is demonstrated by building a real-world application.
The main widgets to be shown are the Toolbar, CList and Tree widgets.
Along the way, some other widgets such as a dialog will be shown as well.
The program to show all this will be a small file explorer. It will not
perform all functions that one would expect from a file explorer, but it
is not meant to be, either. It just demonstrates how one could go about when
making a file explorer.
The File explorer will have 2 main components. One is a directory tree
which can be used to select a directory. the other is a Clist, a component
that presents a list of items in a table with headings. The Clist will be
used to display the files in the directory selected in the directory tree.
The functionality included will be limited to viewing the properties of
a file, and deleting a file. The view can be customized, and sorting of
columns by clicking the column header is possible.
Each window developed in the article will be described in a record, i.e.
all window elements will have a field in a record that points to the
GTK widget used. Several forms will be developed, and each form will be
put in a separate unit. Signal callbacks will in general receive a
'userdata' pointer that points to the window record. This approach mimics
the object oriented approach of GTK, and is similar to the approach in
Delphi, where instead of a object, a window class is used.
\section{The main window}
The main window will consist of a menu, a tool bar, a directory tree and
the file list. The bottom of the screen will contain a statusbar. Between
the directory tree and the file list is a splitter that can be used to
resize the directory tree.
Right-clicking on the file list will show a popup menu, from which file
actions can be selected.
All the widgets in the main window will be stored in a big record
\lstinline|TMainWindow|:
\begin{lstlisting}{}
TMainWindow = Record
FDir,
FMask : String;
Window : PGtkWindow;
Menu : PGtkMenuBar;
Toolbar : PGtkToolBar;
DirTree : PGtkTree;
FileList : PGtkClist;
Pane : PGtkPaned;
StatusBar : PGtkStatusBar;
FilesHeader,DirHeader : PGtkLabel;
// helper objects - Menu
Accel : PGtkAccelGroup;
MFile,
MView,
MColumns,
MHelp,
// Main menu items
PMFiles : PGtkMenu;
MIFile,
MIFileProperties,
MIFileDelete,
MIExit,
MiColumns,
MIAbout,
MIHelp : PGtkMenuItem;
MIShowTitles,
MIShowExt,
MIShowSize,
MiShowDate,
MIShowAttrs : PGtkCheckMenuItem;
// Files PopupMenu Items:
PMIFileProperties,
PMIFileDelete : PGtkMenuItem;
// Packing boxes
VBox,
LeftBox,
RightBox : PGtkBox;
// Scroll boxes
TreeScrollWindow,
ListScrollWindow : PGtkScrolledWindow;
// Tree root node.
RootNode : PGtkTreeItem;
end;
PMainWindow = ^TMainWindow;
\end{lstlisting}
The record resembles a form class definition as used in \lstinline|Delphi|, it
contains all possible widgets shown on the window.
The most important ones are of course the \lstinline|DirTree| and \lstinline|FileList|
fields, the \lstinline|Menu| which will refer to the main menu and the
\lstinline|PMfiles| which will hold the popup menu. The Status bar is of course
in the \lstinline|StatusBar| field, and the \lstinline|ToolBar| field will hold the main
toolbar of the application.
The \lstinline|FDir| field will be used to hold the currently shown
directory and the \lstinline|FMask| field can be used to store a file mask that
determines what files will be shown in the list.
All these fields are filled in using the function \lstinline|NewMainForm| :
\begin{lstlisting}{}
Function NewMainForm : PMainWindow;
\end{lstlisting}
The function starts as follows :
\begin{lstlisting}{}
begin
Result:=New(PMainWindow);
With Result^ do
begin
FMask:='*.*';
Window:=PgtkWindow(gtk_window_new(GTK_WINDOW_TOPLEVEL));
gtk_window_set_title(Window,SFileExplorer);
gtk_widget_set_usize(PgtkWidget(Window),640,480);
gtk_signal_connect (PGTKOBJECT (window), 'destroy',
GTK_SIGNAL_FUNC (@destroy), Result);
gtk_widget_realize(PgtkWidget(window));
\end{lstlisting}
This is a more or less standard GTK setup for a window. Note that the
pointer to the window record is passed to the 'destroy' signal handler
for the window, and that the window widget is realized (so a actual
window is created). The necessity for the 'realize' call is explained below.
After the window is created, the main widgets on the form are created:
\begin{lstlisting}{}
Menu:=NewMainMenu(Result);
ToolBar:=NewToolbar(Result);
StatusBar:=PgtkStatusBar(gtk_statusbar_new);
FileList:=NewFileList(Result);
DirTree:=NewDirtree(Result);
PMFiles:=NewFilePopupMenu(Result);
\end{lstlisting}
The functions used to create these widgets will be discussed further on.
\begin{description}
\item[Menu] The menu is created in the function \lstinline|NewMainMenu|
\item[ToolBar] The toolbar is created in the \lstinline|NewToolbar| function.
\item[FileList] The CList component which will show the file data. Created
using \lstinline|NewFileList|.
\item[DirTree] The directory tree showing the directory structure of the
disk is created using \lstinline|NewDirtree|.
\item[PMFiles] is the popup menu for the file list and is created in the
\lstinline|NewFilePopupMenu| function.
\end{description}
Each function will set the fields which contain the helper widgets.
After the main widgets have been created, it is time to put them on the
form, and the rest of the \lstinline|NewMainForm| function is concerned
mainly with placing the widgets in appropriate containers.
A splitter widget in GTK is called a \lstinline|paned window|. It can be created
using one of the following functions:
\begin{lstlisting}{}
function gtk_hpaned_new : PGtkWidget;
function gtk_vpaned_new : PGtkWidget;
\end{lstlisting}
Since the directory tree and file explorer window will be located left to
each other, a \lstinline|gtk_hpaned_new| call is needed for the file explorer.
The \lstinline|paned window| has 2 halves, in each of which a widget can be
placed. This is done using the following calls:
\begin{lstlisting}{}
procedure gtk_paned_add1(paned:PGtkPaned; child:PGtkWidget);cdecl;
procedure gtk_paned_add2(paned:PGtkPaned; child:PGtkWidget);cdecl;
\end{lstlisting}
The first function adds a widget to the left pane, the second to the right
pane (or the top and bottom panes if the splitter is vertical).
With this knowledge, the Directory Tree and File List can be put on the
form. In the case of the file explorer, 2 widgets will be packed in vertical
boxes which are on their turn put the left and right panes of the splitter:
\begin{lstlisting}{}
Pane:=PgtkPaned(gtk_hpaned_new);
DirHeader:=PgtkLabel(gtk_label_new(pchar(SDirTree)));
LeftBox:=PGtkBox(gtk_vbox_new(false,0));
gtk_box_pack_start(Leftbox,PGtkWidget(DirHeader),False,False,0);
gtk_box_pack_start(Leftbox,PgtkWidget(TreeScrollWindow),true,True,0);
gtk_paned_add1(pane,PGtkWidget(Leftbox));
\end{lstlisting}
The left-hand side vertical box (\lstinline|LeftBox|) contains a label
(\lstinline|DirHeader|) which serves as a heading for the directory tree (\lstinline|DirTree|).
It displays a static text (in the constant \lstinline|SDirTree|).
The right pane can be filled in a similar way with the file list:
\begin{lstlisting}{}
FilesHeader:=PgtkLabel(gtk_label_new(pchar(SFilesInDir)));
RightBox:=PGtkBox(gtk_vbox_new(false,0));
gtk_box_pack_start(Rightbox,PGtkWidget(FilesHeader),False,False,0);
gtk_box_pack_start(Rightbox,PGtkWidget(ListScrollWindow),true,True,0);
gtk_paned_add2(pane,PGtkWidget(Rightbox));
\end{lstlisting}
The right-hand side vertical box contains a label \lstinline|FileHeader|
which serves as a heading for the file list (\lstinline|FileList|).
It will be used to display the current directory name
(\lstinline|SFilesInDir| constant).
After the directory tree and file view have been put in a paned window,
all that is left to do is to stack the statusbar, paned window, toolbar
and menu in a vertical box \lstinline|VBox| which covers the whole window:
\begin{lstlisting}{}
VBox:=PGtkBox(gtk_vbox_new(false,0));
gtk_container_add(PGtkContainer(Window),PgtkWidget(VBox));
gtk_box_pack_start(vbox,PGtkWidget(Menu),False,False,0);
gtk_box_pack_start(vbox,PGtkWidget(ToolBar),False,False,0);
gtk_box_pack_start(vbox,PGtkWidget(Pane),true,true,0);
gtk_box_pack_start(vbox,PGtkWidget(StatusBar),false,false,0);
gtk_widget_show_all(PGtkWidget(vbox));
end;
end;
\end{lstlisting}
The destroy signal of the window does nothing except destroying the
main window record and telling GTK to exit the event loop:
\begin{lstlisting}{}
procedure destroy(widget : pGtkWidget ; Window : PMainWindow); cdecl;
begin
gtk_clist_clear(Window^.FileList);
dispose(Window);
gtk_main_quit();
end;
\end{lstlisting}
The call to \lstinline|gtk_clist_clear| serves to clear the file list window.
The necessity for this call will be explained below.
\section{The file list}
The file list is constructed using the GTK CList widget. This is a powerful
widget that contains a lot of functionality, comparable to the
\lstinline|TListView| component found in Delphi.
A the file list widget is created using the following function:
\begin{lstlisting}{}
Function NewFileList(MainWindow : PMainWindow) : PGtkClist;
Const
Titles : Array[1..6] of pchar =
('Name','ext','Size','Date','Attributes','');
begin
MainWindow^.ListScrollWindow:=
PGtkScrolledWindow(gtk_scrolled_window_new(Nil,Nil));
gtk_scrolled_window_set_policy(MainWindow^.ListScrollWindow,
GTK_POLICY_AUTOMATIC,
GTK_POLICY_AUTOMATIC);
Result:=PGtkClist(Gtk_Clist_New_with_titles(6,@Titles));
gtk_Container_add(PGTKContainer(MainWindow^.ListScrollWindow),
PGtkWidget(Result));
\end{lstlisting}
A Clist object is not capable of drawing scroll bars if it contains too many
items for its size, so first a \lstinline|Scrolled Window| is created in which
the Clist object is embedded. A scrolled window is a container widget which
does nothing except providing scrollbars for the widget it contains.
A scrolled window is created using the \lstinline|gtk_scrolled_window_new|
function:
\begin{lstlisting}{}
function gtk_scrolled_window_new(hadjustment:PGtkAdjustment;
vadjustment:PGtkAdjustment):PGtkWidget
\end{lstlisting}
The \lstinline|Adjustment| parameters can be used to pass scrollbar widgets
that the scrolled window should use to do it's work.
If none are passed, the scrolled window will create the needed scrollbars
by itself.
The visibility of the scrollbars can be controlled with the policy property
of the scrolled window:
\begin{lstlisting}{}
gtk_scrolled_window_set_policy(scrolled_window:PGtkScrolledWindow;
hscrollbar_policy:TGtkPolicyType;
vscrollbar_policy:TGtkPolicyType)
\end{lstlisting}
The horizontal and vertical policies can be set to the following values:
\begin{description}
\item[GTK\_POLICY\_AUTOMATIC] Scrollbars are only visible if they are needed.
\item[GTK\_POLICY\_ALWAYS] Scrollbars are always visible.
\end{description}
After the creation of the scrolled window, the file list is created and
added to the scrolled window. A CList widget can be created using 2 calls;
\begin{lstlisting}{}
function gtk_clist_new (columns:gint):PGtkWidget;
function gtk_clist_new_with_titles (columns:gint;
titles:PPgchar):PGtkWidget;
\end{lstlisting}
In both cases, the number of columns in the list must be passed. If
the column header titles are fixed and known, they can be passed in the
\lstinline|gtk_clist_new_with_titles| call, but they can still be set and
retrieved later on with the following calls:
\begin{lstlisting}{}
Procedure gtk_clist_set_column_title(clist:PGtkCList;
column:gint;
title:Pgchar);cdecl;
function gtk_clist_get_column_title(clist:PGtkCList;
column:gint):Pgchar;cdecl;
\end{lstlisting}
Note that the column indices are 0 based.
After the CList widget has been created, some properties can be set:
\begin{lstlisting}{}
gtk_clist_set_shadow_type(Result,GTK_SHADOW_ETCHED_OUT);
\end{lstlisting}
This call sets the border around the clist. The possible values for
the last parameter (the \lstinline|TGtkShadowType|) of
\lstinline|gtk_clist_set_shadow_type| are:
\begin{description}
\item[GTK\_SHADOW\_NONE] No border.
\item[GTK\_SHADOW\_IN] the clist appears lowered.
\item[GTK\_SHADOW\_OUT] the clist appears raised.
\item[GTK\_SHADOW\_ETCHED\_IN] the clist appears with a lowered frame.
\item[GTK\_SHADOW\_ETCHED\_OUT] the clist appears with a raised frame.
\end{description}
The justification of a column in the list can be set:
\begin{lstlisting}{}
gtk_clist_set_column_justification(result,2,GTK_JUSTIFY_RIGHT);
\end{lstlisting}
column 2 will contain the file sizes, so it is set right-justified.
Other possible values are for justification are
\lstinline|GTK_JUSTIFY_LEFT|, \lstinline|GTK_JUSTIFY_CENTER|, and
\lstinline|GTK_JUSTIFY_FILL|, which have their obvious meanings.
To be able to select multiple items (or rows) at once, the selection mode of
the CList must be set:
\begin{lstlisting}{}
gtk_clist_set_selection_mode(Result,GTK_SELECTION_MULTIPLE);
\end{lstlisting}
Possible modes of selection are:
\begin{description}
\item[GTK\_SELECTION\_SINGLE] Only one row can be selected at any given
time.
\item[GTK\_SELECTION\_BROWSE] Multiple items can be selected, however the
selection will always return 1 item.
\item[GTK\_SELECTION\_MULTIPLE] Multiple items can be selected, and the
selection will contain all selected items.
\item[GTK\_SELECTION\_EXTENDED] The selection is always \lstinline|Nil|.
\end{description}
The selection is a field (\lstinline|selection|) of type \lstinline|PGList| in the
\lstinline|TGtkCList| record. A \lstinline|PGlist| is a pointer to a doubly linked
list with data pointers. More details about this will follow.
The elements in the list list can be sorted.
\begin{lstlisting}{}
gtk_clist_set_auto_sort(Result,True);
If DefCompare=Nil then
DefCompare:=Result^.compare;
gtk_clist_set_compare_func(Result,
TGtkCListCompareFunc(@FileCompareFunc));
\end{lstlisting}
By default, a CList sorts by comparing the texts in the current sort column
of the items in the list. This sorting happens using the \lstinline|compare|
function of the CList. The standard \lstinline|compare| function of the list
is saved here in a variable \lstinline|DefCompare|, so it can still be used.
Using the \lstinline|gtk_clist_set_compare_func| the compare function to be
used when sorting can be set, and it is set to the function
\lstinline|FileCompareFunc|, which will be discussed later on.
The \lstinline|gtk_clist_set_auto_sort| can be used to set the auto-sort
feature of the Clist. If auto-sort is on, adding new items to the CList will
insert them in the correct order. If auto-sort is off, new items are
appended to the beginning or end of the list.
After the sort function is set, handlers are attached to 2 signals:
\begin{lstlisting}{}
gtk_signal_connect(PgtkObject(Result),'button_press_event',
TGtkSignalFunc(@ShowPopup),MainWindow);
gtk_signal_connect(PgtkObject(Result),'click_column',
TGtkSignalFunc(@FileColumnClick),MainWindow);
\end{lstlisting}
The first handler connects to a mouse button press event. This will be used
to detect a right mouse click, and to show a popup menu:
\begin{lstlisting}{}
Procedure ShowPopup(Widget : PGtkWidget;
Event : PGdkEventButton;
Window : PMainWindow);cdecl;
begin
if (event^.thetype=GDK_BUTTON_PRESS) and
(event^.button=3) then
gtk_menu_popup(Window^.PMFiles,Nil,Nil,Nil,NIl,3,event^.time);
end;
\end{lstlisting}
The \lstinline|gtk_menu_popup| function does nothing but showing the menu;
when a menu item is clicked, the menu will close by itself.
The second handler connects to the 'click\_column' event. This event is
emitted if the user clicks on the column header. It will be used to switch
the sort order of the file list:
\begin{lstlisting}{}
Procedure FileColumnClick(List : PGtkCList;Column:gint; Window : PMainWindow);cdecl;
Var
I : longint;
NS : TGtkSortType;
begin
If Column<>List^.sort_column Then
begin
gtk_clist_set_sort_type(List,GTK_SORT_ASCENDING);
gtk_clist_set_sort_column(list,Column);
end
else
begin
If (List^.Sort_type=GTK_SORT_ASCENDING) Then
NS:=GTK_SORT_DESCENDING
else
NS:=GTK_SORT_ASCENDING;
gtk_clist_set_sort_type(List,NS);
end;
gtk_clist_sort(list);
end;
\end{lstlisting}
The function starts by retrieving the current sort column. If it is
different from the column the used clicked on, then 2 things are done:
\begin{enumerate}
\item The sort type is set to ascending.
\item The sort column is set to the column the user clicked.
\end{enumerate}
If, on the other hand, the user clicks on a column that is the sort column,
the sort type is simply reversed. After the sort column and sort type are
set, the list is epxlicitly sorted. (neither of the calls that set the sort
order or sort column forces a sort).
The sort happens using the \lstinline|compare| function (\lstinline|FileCompareFunc|)
that was set when the CList was created:
\begin{lstlisting}{}
Function FileCompareFunc(List:PGtkCList; Row1,Row2 : PGtkCListRow) : Longint; Cdecl;
Var
SC : Longint;
begin
SC:=List^.sort_column;
If SC in [2,3] then
begin
SC:=SC-2;
Result:=PLongint(Row1^.Data)[SC]-PLongint(Row2^.Data)[SC];
end
Else
Result:=DefCompare(List,Row1,Row2);
end;
\end{lstlisting}
This function receives 3 arguments:
\begin{itemize}
\item The list that needs to be sorted.
\item 2 pointers to the row objects that must be compared.
\end{itemize}
The result must be an integer that is negative if the first row should come
before the second or larger than zero if the second row should come before
the first. If the result is zero then the columns are considered the same.
The function checks what the sort column is. If it is not the size (2) or
date (3) column, then the default row compare function (which was saved in
the \lstinline|DefCompare| variable when the list was created) is used to
compare the rows. If the size or date columns must be compared, the user
data associated with the rows is examined. As will be shown below, the user
data will point to an array of 2 Longint values that describe the size and
datestamp of the file. The approriate values are compared and the result is
passed back.
To fill the file list with data, the \lstinline|FillList| function is
implemented:
\begin{lstlisting}{}
Function FillList(List : PGtkCList;
Const Dir,Mask : String) : Integer;
Var
Info : TSearchRec;
Size : Int64;
I,J : longint;
begin
Result:=0;
Size:=0;
gtk_clist_freeze(List);
Try
gtk_clist_clear(List);
If FindFirst (AddTrailingSeparator(Dir)+Mask,
faAnyFile,Info)=0 then
Repeat
Inc(Size,Info.Size);
AddFileToList(List,Info);
Inc(Result);
Until FindNext(Info)<>0;
FindClose(info);
finally
For I:=0 to 4 do
begin
J:=gtk_clist_optimal_column_width(List,i);
gtk_clist_set_column_width(List,i,J);
end;
gtk_clist_thaw(List)
end;
end;
\end{lstlisting}
This function is very straightforward. To start, it 'freezes' the list with
\lstinline|gtk_clist_freeze|; this prevents the list from updating the
screen each time a row is added or deleted. Omitting this call would cause
serious performance degradation and screen flicker.
After freezing the list, it is cleared; Then a simple loop is implemented
that scans the given directory with the given file mask using the
\lstinline|FindFirst|/\lstinline|FindNext| calls. For each file found
it calls the \lstinline|AddFileToList| function, that will actually add the
file to the list view, using the information found in the search record.
The \lstinline|AddTrailingSeparator| adds a directory separator to a
string containing the name of a directory if this does not end on a
separator yet. It can be found in the \file{futils} unit.
After the loop has finished, the optimal width for each column is
retrieved using the \lstinline|gtk_clist_optimal_column_width| function
and the result is used to set the column width. As a result, the columns will
have the correct size for displaying all items.
When this has been done, the list is 'thawed' with \lstinline|gtk_clist_thaw|,
which means that it will repaint itself if needed. This happens in a
\lstinline|finally| block since the \lstinline|gtk_clist_freeze| and
\lstinline|gtk_clist_thaw| work with a reference counter. For each 'freeze'
call the counter is increased. It is decreased with a 'thaw' call. When the
counter reaches zero, the list is updated.
The function that actually adds a row to the list view is quite simple:
\begin{lstlisting}{}
Procedure AddFileToList(List : PGtkCList; Info : TSearchRec);
Var
Texts : Array[1..6] of AnsiString;
FSD : PLongint;
I : longint;
begin
Texts[1]:=ExtractFileName(Info.Name);
Texts[2]:=ExtractFileExt(Info.Name);
Texts[3]:=FileSizeToString(Info.Size);
Texts[4]:=DateTimeToStr(FileDateToDateTime(Info.Time));
Texts[5]:=FileAttrsToString(Info.Attr);
Texts[6]:='';
i:=gtk_clist_append(List,@Texts[1]);
FSD:=GetMem(2*SizeOf(Longint));
FSD[0]:=Info.Size;
FSD[1]:=Info.Time;
gtk_clist_set_row_data_full (List,I,FSD,@DestroySortData);
end;
\end{lstlisting}
The \lstinline|gtk_clist_append| call accepts 2 paramers: a CList, and a
pointer to an array of zero-terminated strings. The array must contain as
much items as the CList has columns (in the above, the last column is
always empty, as this gives a better visual effect). The call adds a column
at the end of a list; An item can be inserted at the beginning of the list
with \lstinline|gtk_clist_append|, which accepts the same parameters. An
item can be inserted at certain position:
\begin{lstlisting}{}
gtk_clist_insert(clist:PGtkCList; row:gint; thetext:PPgchar);cdecl;
\end{lstlisting}
Note that all these calls do the same thing if the 'auto sort' was set for
the CList.
The \lstinline|FileAttrsToString| function converts file attributes to a
string of characters that indicate whether a given attribute is present.
It can be found in the \file{futils} unit and will not be shown here.
After the file data was appended to the CList, an array of 2 longints is
allocated on the heap. The first longint is filled with the size of the
file, the second with the date of the file. The pointer to this array is
then associated with the row that was just inserted with the
\lstinline|gtk_clist_set_row_data_full| call. There are 2 calls to
associate data with a row:
\begin{lstlisting}{}
gtk_clist_set_row_data(clist:PGtkCList;
row:gint;
data:gpointer);cdecl;
gtk_clist_set_row_data_full(clist:PGtkCList;
row:gint; data:gpointer;
destroy: :TGtkDestroyNotify);
\end{lstlisting}
the first call is used to add data to a clist that will not need to be
destroyed if the row is deleted. The second call can be used to pass a
callback that will be called when the row is destroyed.
In the case of the file list, the \lstinline|DestroySortData| call is
used to dispose the array with sort data:
\begin{lstlisting}{}
Procedure DestroySortData(FSD : Pointer);cdecl;
begin
FreeMem(FSD);
end;
\end{lstlisting}
The reason that the file list is cleared when the main window is destroyed
now becomes apparent: when the list is cleared, all data associated with
the file list is freed. If the call to \lstinline|gtk_clist_clear| is
omitted before destroying the main window, the list is not cleared and all
data stays in memory even after the window closes.
The display of the column titles of the file list can be switched on or off.
To do this a check menu item ('Hide titles') is added to the 'View' menu.
If the menu is clicked, the following callback is executed:
\begin{lstlisting}{}
Procedure ToggleFileListTitles(Sender : PGtkCheckMenuItem;
Window : PMainWindow);cdecl;
begin
If active(Sender^)=0 then
gtk_clist_column_titles_show(Window^.FileList)
else
gtk_clist_column_titles_hide(Window^.FileList)
end;
\end{lstlisting}
The \lstinline|active| function checks whether a check menu item is currently
checked ot not and shows or hides the titles.
Not only can the column titles be switched on or off, it is also possible to
control whether or not a given column must be shown;
Under the 'View' menu, there is a 'Hide columns' submenu that contains 4
check menus that can be used to toggle the visibility of the columns in the
file list. All the check menu items are connected to the following callback:
\begin{lstlisting}{}
Procedure ToggleFileListColumns(Sender : PGtkCheckMenuItem;
Window : PMainWindow);cdecl;
Var Col : Longint;
begin
With Window^ do
If Sender=MIShowExt Then
Col:=1
else if Sender=MiShowSize Then
Col:=2
else if Sender=MIShowDate then
Col:=3
else
Col:=4;
gtk_clist_set_column_visibility(Window^.FileList,
Col,
(Active(Sender^)=0));
end;
\end{lstlisting}
The call gets as 'user data' a pointer to the main window record. Using this
it checks which menu emitted the call, and updates the corresponding column
with the \lstinline|gtk_clist_set_column_visibility| function.
More attributes of a CList can be set, but they will not be discussed here;
the GTK documentation and tutorial offer an overview of the possibilities.
The selection mode of the CList has been set to allow selection of multiple
rows. The Clist maintains a linked list (A Glist) with the rows that are
part of the selection. The linked list contains the indexes of the selected
rows in it's associated data.
The linked list \lstinline|Glist| is often used in GTK applications.
It consists of the following records:
\begin{lstlisting}{}
TGList = record
data gpointer;
next,prev : PGlist;
end;
PGlist=^TGlist;
\end{lstlisting}
The selection of a CList is of type \lstinline|PGlist|. The \lstinline|data|
pointer can be typecasted to an integer to return the index of a selected
row.
The following function walks the selection linked list and stores the
associated filenames in a \lstinline|TStrings| class:
\begin{lstlisting}{}
Procedure GetFileSelection (List : PGtkClist; Selection : TStrings);
Var
SList : PGList;
Index : Longint;
P : PChar;
begin
Selection.Clear;
Slist:=List^.Selection;
While SList<>nil do
begin
Index:=Longint(SList^.Data);
gtk_clist_get_text(List,Index,0,@p);
Selection.Add(StrPas(p));
SList:=g_list_next(SList);
end;
end;
\end{lstlisting}
The \lstinline|gtk_clist_get_text| retrieves the text of a given cell in the
CList (a similar function exists to set the text) , and the
\lstinline|g_list_next| jumps to the next element in the linked list.
The \lstinline|TStrings| class is the standard string container as defined
in the \lstinline|Classes| unit of Free Pascal (or Delphi).
The above function will be used to retrieve the list of selected files so
operations can be done on the selection.
To retrieve the first (and possibly only) item of a selection, and the
number of items in a selection, the following functions can be used:
\begin{lstlisting}{}
Function GetFileFirstSelection (List : PGtkClist) : String;
Var
SList : PGList;
Index : Longint;
P : PChar;
begin
Result:='';
Slist:=List^.Selection;
If SList<>nil then
begin
Index:=Longint(SList^.Data);
gtk_clist_get_text(List,Index,0,@p);
Result:=StrPas(p);
end;
end;
Function GetFileSelectionCount (List : PGtkClist) : Longint;
Var
SList : PGList;
begin
Slist:=List^.Selection;
Result:=0;
While SList<>nil do
begin
Inc(Result);
SList:=g_list_next(SList);
end;
end;
\end{lstlisting}
These functions will be used further on.
The filelist is now ready to be used. To be able to select a directory from
which the files should be displayed, a Tree widget is used. How to create
this tree and connect it to the file list is explained in the next section.
\section{The directory tree}
The directory tree will allow the user to browse through the directories on
his system. When a directory is selected, the file view should be updated
to show the files in the selected directory.
To make the directory tree more efficient and less memory consuming, the
tree is not filled with the whole directory tree at once. Instead, only 2
levels of directories will be put in the tree. The tree is progessively
filled as the user expands the directory nodes.
The directory tree is created in the following function:
\begin{lstlisting}{}
Function NewDirtree (MainWindow : PMainWindow) : PGtkTree;
begin
Result:=PGtkTree(gtk_tree_new());
With MainWindow^ do
begin
TreeScrollWindow:=PGtkScrolledWindow(gtk_scrolled_window_new(Nil,Nil));
gtk_widget_show(PGtkWidget(TreeScrollWindow));
gtk_scrolled_window_set_policy(TreeScrollWindow,
GTK_POLICY_AUTOMATIC,
GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_add_with_viewport(TreeScrollWindow,PGtkWidget(Result));
RootNode:=PGtkTreeItem(gtk_tree_Item_new_with_label(Pchar(PathSeparator)));
gtk_tree_append(Result,PgtkWidget(RootNode));
scandirs(PathSeparator,Result, RootNode,True,MainWindow);
gtk_tree_item_expand(rootnode);
end;
end;
\end{lstlisting}
The function starts off by creating the tree widget which is the return
value of the function.
Similar to the Clist, the tree widget does not possess functionality
for displaying scroll bars, so a 'scrolled window' is created,
in which the tree widget is placed.
A tree can have one or more tree items connected to it. Each of these tree
items can in turn have a tree associated with it, which in turn can again
have tree items associated. This way the tree is recursively constructed.
The directory tree is filled with 1 tree item, which will represent the root
directory of the disk which is browsed with the file explorer; The
\lstinline|gtk_tree_item_new_with_label| call returns a new tree item,
which is then appended to the tree using the \lstinline|gtk_tree_append|
call.
After this is done, the directories below the root directory are scanned and
appended to the root node in the \lstinline|scandirs| function, explained
below. If the root node was filled, then it is expanded with
\lstinline|gtk_tree_item_expand| (it can be collapsed with
\lstinline|gtk_tree_item_collapse|)
The \lstinline|scandirs| function scans a given directory for subdirectories
and appends each directory to a subtree of a given node. The subtree is
created if needed:
\begin{lstlisting}{}
Procedure Scandirs(Path: String; Tree : PgtkTree;
Node: PGtkTreeItem ; SubSub : Boolean;
Window : PMainWindow);
Var
NewTree : PGtkTree;
NewNode : PGtkTreeItem;
Info : TSearchRec;
S,FP : AnsiString;
begin
NewTree:=Nil;
FP:=AddTrailingSeparator(Path);
If FindFirst(FP+'*.*',faAnyfile,Info)=0 then
Try
repeat
If ((Info.Attr and faDirectory)=faDirectory) then
begin
S:=Info.Name;
If (S<>'.') and (S<>'..') then
begin
If (Node<>Nil) then
begin
If (NewTree=Nil) and (node<>Nil) then
begin
NewTree:=PGtkTree(gtk_tree_new);
gtk_tree_item_set_subtree(Node,PGtkWidget(NewTree));
end
end
else
NewTree:=Tree;
NewNode:=PGtkTreeItem(gtk_tree_item_new_with_label(Pchar(S)));
gtk_tree_append(NewTree,PgtkWidget(NewNode));
gtk_signal_connect(PGtkObject(NewNode),'select',
TGtkSignalFunc(@DirSelect),Window);
gtk_signal_connect(PGtkObject(NewNode),'expand',
TGtkSignalFunc(@DirExpand),Window);
If SubSub then
ScanDirs(FP+S,Tree,NewNode,False,Window);
gtk_widget_show(PGtkWidget(NewNode));
end;
end;
until FindNext(Info)<>0;
Finally
FindClose(Info);
end;
gtk_widget_show(PGtkWidget(Node));
end;
\end{lstlisting}
The routine is a simple loop. If a subdirectory is found then a new
tree widget is created (\lstinline|newTree|) and appended to the
given node with the \lstinline|gtk_tree_item_set_subtree| call.
For each found subdirectory a new treeitem is created and appended to
the subtree. 2 signals handlers are connected to the created tree item,
one for 'select' signal which is emitted when the user selects a tree item,
and one for the 'expand' signal which is emitted when the user expands a
node. Each of these handlers gets as data a pointer to the main window
record.
The \lstinline|SubSub| parameter is used to control the recursive behaviour.
If it is set to \lstinline|True|, the \lstinline|Scandirs| function will
call itself recursively, but only once. As a result only 2 levels of
subdirectories are scanned.
Finally, the created nodes are shown.
When the user expands a node, the \lstinline|DirExpand| function is
called:
\begin{lstlisting}{}
Procedure DirExpand(Item : PGtkTreeItem; Window : PMainWindow);cdecl;
Var
Dir : String;
SubTree : PGtkTree;
SubNodes : PGList;
Node : PGtkTreeItem;
begin
SubTree:=PgtkTree(Item^.SubTree);
SubNodes:=gtk_container_children(PGtkContainer(SubTree));
While SubNodes<>Nil do
begin
Node:=PgtkTreeItem(SubNodes^.Data);
If (Node^.SubTree<>Nil) then
gtk_tree_item_remove_subtree(Node);
Scandirs(GetPathName(Node),Nil,Node,False,Window);
SubNodes:=g_list_remove_link(SubNodes,SubNodes);
end;
end;
\end{lstlisting}
The function starts by retrieving the subtree of the tree item that
triggered the callback. It then retrieves the list of subnodes (treeitems)
of the subtree which represent the subdirectories of the directory node
that is about to be expanded. The Tree object descends from the GTK
container object, and keeps its treeitems in the container's children
list. This list is a Glist. The \lstinline|gtk_container_children| returns
a copy of the list containing the children.
Then a simple loop is executed: for each of
the found nodes, the subtree is destroyed if it exists:
\lstinline|gtk_tree_item_remove_subtree| removes a subtree from a treeItem
and destroys it.
After the subtree is destroyed, at the subirectory is scanned for possible
subdirecties (remark that the \lstinline|SubSub| parameter is set to
\lstinline|false|) and the subtree is recreated if needed.
The directory corresponding to a given node is calculated in the
\lstinline|GetPathName| function, explained below.
The next cycle of the loop is started by removing and destroying the first
element of the GList with the \lstinline|g_list_remove_link| call:
the call returns the new start of the list with the element removed. By
passing the first element of the list as the element to be removed the
whole list is traversed.
When the user selects a tree item, the list view must be updated with
the files in that directory. This is done in the \lstinline|DirSelect|
handler for the 'select' signal:
\begin{lstlisting}{}
Procedure DirSelect(Item : PGtkTreeItem; Window : PMainWindow);cdecl;
begin
ShowDir(Window,GetPathName(Item));
end;
Procedure ShowDir (Window : PMainWindow; Dir : String);
begin
With Window^ do
begin
FDir:=Dir;
FillList(FileList,Dir,FMask);
gtk_label_set_text(FilesHeader,pchar(Format(SFilesInDir,[Dir])));
end;
end;
\end{lstlisting}
The \lstinline|Showdir| function will be called from other places as
well hence it is put separately; The \lstinline|DirSelect| function
does nothing but to call the ShowDir function after it has calculated the
path of the treeitem that triggered the 'select' signal:
\begin{lstlisting}{}
Function GetPathName(Item : PGtkTreeItem) : String;
Var P : PChar;
PTree : PGtkTree;
begin
gtk_label_get(PgtkLabel(PGtkBin(Item)^.Child),@P);
Result:=StrPas(P);
If (PGtkWidget(item)^.Parent<>Nil) then
begin
PTree:=PGtkTree(PgtkWidget(Item)^.Parent);
If (Ptree^.Level<>0) Then
Result:=AddTrailingSeparator(GetPathName(PgtkTreeItem(PTree^.Tree_Owner)))+Result
end;
end;
\end{lstlisting}
It is a simple recursive mechanism. The only issue with this
routine is that one should know that the parent of a tree item is a tree,
and that the owner of the tree (in it's \lstinline|Tree_Owner| field) is
in turn again a treeitem. The \lstinline|Level| field of a tree determines
at what level the tree is located (i.e. the number of nodes present above
the tree) and can be used to check when the algorithm should stop.
An alternate approach would have been to associate with each node some
user data, such as a string that is the full path name of the node.
With this, the tree is created and is linked to the file list, so the
user has the capability to select any directory and display it's contents;
The user can also customize the view of the file list.
However, no actions can be performed on the files. This is treated in the
next sections, where a toolbar and popup menu are used to allow the user to
do things with the shown files.
\section{Adding a popup menu}
To allow the user to do something with the displayed files, a popup menu is
addd to the file list. Adding a popup menu is not different from adding a
main menu to a form, just it will not be attached to a menu bar. The popup
menu will be hidden till the user right-clicks in the file list.
The popup menu is created in the following function:
\begin{lstlisting}{}
Function NewFilePopupMenu (MainWindow : PMainWindow) : PGtkMenu;
begin
result:=PGtkMenu(gtk_menu_new);
gtk_signal_connect(PGtkObject(result),'show',
TGtkSignalFunc(@PMFilesActivate),MainWindow);
With MainWindow^ do
begin
PMIFileProperties:=AddItemToMenu(Result,Accel,'_Properties','',
TgtkSignalFunc(@DoProperties),
MainWindow);
PMIFileDelete:=AddItemToMenu(Result,Accel,'_Delete','<ctrl>d',
TgtkSignalFunc(@DeleteFile),
MainWindow);
end;
end;
\end{lstlisting}
The \lstinline|AddItemToMenu| functions were developed in an earlier
articles, and have been collected in the 'menus' unit.
The 'show' handler attached to the menu is used to set the state
of some of the menu items when the menu pops up:
\begin{lstlisting}{}
Procedure PMFilesActivate(Widget : PGtkWidget; Window : PMainWindow); cdecl;
Var State : TGtkStateType;
begin
if GetFileSelectionCount(Window^.FileList)>1 then
State:=GTK_STATE_INSENSITIVE
else
State:=GTK_STATE_Normal;
gtk_widget_set_state(PgtkWidget(Window^.PMIFileProperties),State);
end;
\end{lstlisting}
When more than 1 file is selected in the file view, the properties menu item
is disabled.
The popup menu will appear if the user clicks the right button in the file
list; The necessary event handler for that (\lstinline|ShowPopup|) was
attached to the CList and discussed earlier on.
The delete menu item has the following 'click' handler:
\begin{lstlisting}{}
Procedure DeleteFile(Widget : PGtkWidget; Window : PMainWindow); cdecl;
Var i : longint;
S : TStringList;
begin
S:=TStringList.Create;
Try
GetFileSelection(Window^.FileList,S);
For I:=0 to S.Count-1 do
begin
For I:=0 to S.Count-1 do
SysUtils.DeleteFile(Window^.FDir+S[i]);
end;
Finally
If S.Count>0 then
RefreshFileView(Window);
S.Free;
end;
end;
\end{lstlisting}
The routine simply retrieves the selection list and deletes all files
present in it; After that the file view is refreshed.
The properties popup menu action will be treated later on.
\section{Adding a toolbar}
The toolbar in the file explorer application will contain 2 buttons with
a pixmap on them; the pixmap will be loaded from data compiled into the
binary. The actions performed by the toolbar buttons will be the same as
the actions in the popup menu: show a file's properties and delete the file.
The creation of the toolbar for the file explorer program is done in the
following function:
\begin{lstlisting}{}
Function NewToolbar (MainWindow : PMainWindow) : PGtkToolbar;
begin
Result:=pGtkToolBar(gtk_toolbar_new(GTK_ORIENTATION_HORIZONTAL,
GTK_TOOLBAR_ICONS));
gtk_toolbar_append_item(result,
Nil,
'File Properties',
nil,
CreateWidgetFromXPm(PgtkWidget(MainWindow^.Window),
@PropertiesXPM),
TgtkSignalFunc(@DoProperties),
MainWindow);
gtk_toolbar_append_item(result,
Nil,
'Delete File',
Nil,
CreateWidgetFromXPm(PgtkWidget(MainWindow^.Window),
@DeleteXPM),
TgtkSignalFunc(@DeleteFile),
MainWindow);
end;
\end{lstlisting}
The \lstinline|gtk_toolbar_new| function creates a new toolbar. The first
argument to this call specifies the orientation for the toolbar. Possible
values for the orientation are:
\begin{description}
\item[GTK\_ORIENTATION\_HORIZONTAL] The toolbar is filled horizontally;
\item[GTK\_ORIENTATION\_VERTICAL] The toolbar is filled vertically;
\end{description}
The second argument determines the style of the toolbar; it can have the
following values:
\begin{description}
\item[GTK\_TOOLBAR\_TEXT] Toolbuttons just show a text.
\item[GTK\_TOOLBAR\_ICONS] Toolbuttons just show a pixmap.
\item[GTK\_TOOLBAR\_BOTH] toolbuttons show both a pixmap and text.
\end{description}
The style determines what widgets will be placed on new toolbuttons that
are added with the \lstinline|gtk_toolbar_append_item| or
\lstinline|gtk_toolbar_prepend_item| calls. If buttons are added to the
toolbar manually, the style has no effect.
The \lstinline|gtk_toolbar_append_item| call adds a new toolbar button
to the end of a toolbar. The \lstinline|gtk_toolbar_prepend_item| item
inserts a new button at the beginning of the toolbar. Both accept the
following arguments:
\begin{enumerate}
\item a pointer to the toolbar to which the item should be added.
\item a zero-terminated string with the text to be shown on the button.
\item a zero-terminated string with the tooltip text (the hint) for the button.
\item a zero terminated private tooltip text for the button.
\item an icon wiget, usually a GtkPixmap.
\item A callback function of type \lstinline|TGtkSignalFunc| that will be
executed when the user clicks the button.
\item Callback data pointer which will be passed to the callback.
\end{enumerate}
A toolbutton can also be inserted at a certain position with the
\lstinline|gtk_toolbar_insert_item| call. It accepts an additional (last)
argument, the position at which to insert the toolbutton.
For the toolbar of the file explorer program, the buttons contain no text
(since the \lstinline|GTK_TOOLBAR_ICONS| style was chosen for the toolbar)
they do contain an icon, a pixmap widget.
The pixmap widget is created with the following function:
\begin{lstlisting}{}
function CreateWidgetFromXPM (Window : PGtkWidget;
Data : PPChar) : PGtkWidget;
Var
mask : PGdkBitmap;
pixmap : PGdkPixMap;
begin
pixmap:=gdk_pixmap_create_from_xpm_d(window^.window,@mask,nil,ppgchar(Data));
Result:=gtk_pixmap_new(Pixmap,Mask);
gtk_widget_show(Result);
end;
\end{lstlisting}
This function accepts 2 arguments: A GTK window, and a pointer to an array
or zero-terminated strings which describe the pixmap. With these it creates
a gdk pixmap object with the \lstinline|gdk_pixmap_create_from_xpm_d| call.
this function expects the following arguments:
\begin{enumerate}
\item A pointer to a GDK window object. In the above, the GDK window of the
main window widget is used. This explains why the \lstinline|gtk_widget_realize|
call was made when creating the main window: When the widget is realized, a
window is allocated to it. If the main window widget was not realized, then
it's gdk window would be nil.
\item The address of a \lstinline|PGdkBitmap| which will be used to store
the mask of the created pixmap. The mask determines the transparent items
in the bitmap, and can be used when creating a pixmap widget. This may be
nil.
\item A pointer to a color that should be considered the transparent
color. This may be nil, in which case a default color is used.
\item A pointer to a XPM pixmap structure.
\end{enumerate}
After the GDK pixmap and the mask were created, a pixmap widget is created
from the GDK bitmap, and the widget is shown.
The pixmap data is in XPM format. The XPM format is an array of
zero-terminated strings which are organized as follows:
\begin{enumerate}
\item A string describing the pixmap dimensions and the number of colors.
The string is of the form
\begin{verbatim}
'width height #colors chars/color',
\end{verbatim}
So the string
\begin{verbatim}
'16 16 4 1'
\end{verbatim}
means a 16x16 bitmap, using 4 colors, described by 1 character per color.
\item A series of strings that describe the color. the number of strings
should equal the count specified in the first string. The color descriptions
should have the following form:
\begin{verbatim}
'X c #YYYYYY'
\end{verbatim}
here 'X' must be replaced by N characters, where N is the number of
characters per color that was specified in the first string. The YYYYYY
is a RGB color value, in hex format. Each red,green or blue value must
contain 2 or 4 characters. The string '\#FF0000' would describe red, just as
'\#FFFF00000000' would describe red.
Instead of a rgb value, 'None' can be specified to indicate a transparent
color.
Some examples of valid colors would be:
\begin{verbatim}
'. c #000000', { Black }
'# c #000080', { Dark Blue }
'a c None', { Transparent }
'b c #f8fcf8', { greyish }
\end{verbatim}
\item A series of strings of characters, each string describes one line of
the pixmap and is composed of the color characters described in the color
section. Each line has the same length, namely the width of the image
multiplied with the number of characters per color. Obviously, there
should be as many strings as the height of the pixmap.
\end{enumerate}
The \file{fxbitmaps} unit contains 2 such bitmaps; comments have been added.
After the toolbar has been added, the main form is finished. The
form in action is shown in figure \ref{fig:mainwin}.
\begin{figure}[ht]
\caption{The main window in action.}\label{fig:mainwin}
\epsfig{file=gtk4ex/mainwin.png,width=\textwidth}
\end{figure}
The toolbar contains a button to show the properties dialog. This dialog
will show the various properties of a file, and is discussed in the next
section.
\section{Adding some dialogs}
Adding some dialogs to the file explorer program is not so difficult.
Three are created, an about dialog, a file properties dialog, and a dialog
that allows to enter a file mask which will then be applied to the file
view. All three dialogs will be based on the standard GTK dialog.
Adding a dialog that shows the properties of a file is quite easy.
The standard GTK dialog widget contains 3 widgets; a vertical box
(\lstinline|vbox|) which can be used to drop widgets in, a separator
and a horizontal box (\lstinline|action_area|), which can be used to
put buttons (such as an 'OK' button) in.
The file properties dialog consists mainly of a table packed with labels and
some checkboxes. It is created in the following function:
\begin{lstlisting}{}
Type
TFilePropertiesDialog = Record
Window : PgtkDialog;
Table : PGtkTable;
OkButton : PGtkButton;
Labels : Array[0..1,0..NrTableLines] of PGtkLabel;
CheckBoxes : Array[CheckBoxLineStart..NrTableLines] of PgtkCheckButton;
end;
PFilePropertiesDialog = ^TFilePropertiesDialog;
Function NewFilePropertiesDialog(FileName : String) : PFilePropertiesDialog;
Const
CheckAttrs : Array [CheckBoxLineStart..NrTableLines] of Integer
= (faReadOnly,faArchive,faHidden,faSysFile);
Var
Info : TSearchRec;
I : Longint;
begin
Result:=New(PFilePropertiesDialog);
With Result^ do
begin
Window:=PgtkDialog(gtk_dialog_new);
gtk_window_set_title(PgtkWindow(Window),SPropsTitle);
gtk_window_set_modal(PgtkWindow(Window),True);
gtk_window_set_policy(PgtkWindow(Window),0,0,0);
gtk_window_set_position(PGtkWindow(Window),GTK_WIN_POS_CENTER);
OkButton:=PGtkButton(gtk_button_new_with_label(SOK));
gtk_box_pack_start(PgtkBox(Window^.action_area),PGtkWidget(Okbutton),False,False,5);
gtk_window_set_focus(PGtkWindow(Window),PGtkWidget(OkButton));
gtk_widget_show(PGtkWidget(OkButton));
\end{lstlisting}
The above are standard things: The dialog window title is set, the dialog is
made modal, the resizing of the window is prohibited with the
\lstinline|gtk_window_set_policy| call. Then the window is told that it
should position itself in the center of the screen with the
\lstinline|gtk_window_set_position| call. The position specifier can be one
of the following:
\begin{description}
\item[GTK\_WIN\_POS\_NONE] The window manager will decide where the window
goes.
\item[GTK\_WIN\_POS\_CENTER] The window is placed at the center of the
screen.
\item[GTK\_WIN\_POS\_MOUSE] The window is placed where the mouse cursor is.
\end{description}
After the window properties have been set, an OK button is placed in the
action area, and it gets the focus.
Next, a table is created with \lstinline|NrTableLines+1| rows and 2 columns,
and put in the vbox area:
\begin{lstlisting}{}
Table:=PgtkTable(gtk_table_new(NrTableLines+1,2,TRUE));
gtk_box_pack_start(PGtkBox(Window^.vbox),PGtkWidget(Table),True,True,10);
\end{lstlisting}
Then the table is filled with labels that describe the various properties;
the left column contains labels that simplu
\begin{lstlisting}{}
For I:=0 to NrTableLines do
begin
Labels[0,i]:=PGtkLabel(gtk_label_new(LabelTexts[i]));
gtk_label_set_justify(Labels[0,I],GTK_JUSTIFY_RIGHT);
gtk_table_attach_defaults(Table,PgtkWidget(Labels[0,I]),0,1,I,I+1);
end;
For I:=0 to CheckboxLineStart-1 do
begin
Labels[1,i]:=PGtkLabel(gtk_label_new(''));
gtk_label_set_justify(Labels[1,I],GTK_JUSTIFY_LEFT);
gtk_table_attach_defaults(Table,PgtkWidget(Labels[1,I]),1,2,I,I+1);
end;
\end{lstlisting}
The file attributes will be represented with checkboxes:
\begin{lstlisting}{}
For I:=CheckboxLineStart to NrTableLines do
begin
checkBoxes[i]:=PgtkCheckButton(gtk_check_button_new_with_label(CheckBoxTexts[I]));
gtk_widget_set_state(PGtKWidget(CheckBoxes[i]),GTK_STATE_INSENSITIVE);
gtk_table_attach_defaults(Table,PgtkWidget(CheckBoxes[i]),1,2,I,I+1);
end;
\end{lstlisting}
The checkboxes are made inactive, so the user cannot change them.
After all labels and checkboxes are put in place, the file information
is put into various places:
\begin{lstlisting}{}
gtk_label_set_text(Labels[1,0],PChar(ExtractFileName(FileName)));
gtk_label_set_text(Labels[1,1],PChar(ExtractFilePath(FileName)));
gtk_label_set_text(Labels[1,2],PChar(ExtractFileExt(FileName)+SFile));
If FindFirst(FileName,faAnyFile,Info)=0 Then
begin
gtk_label_set_text(Labels[1,3],PChar(FileSizeToString(Info.Size)));
gtk_label_set_text(Labels[1,4],PChar(DateTimeToStr(FileDateToDateTime(Info.Time))));
For I:=CheckboxLineStart to NrTableLines do
If (CheckAttrs[i] and Info.Attr)=CheckAttrs[i] then
gtk_toggle_button_set_active(PgtkToggleButton(CheckBoxes[I]),True);
FindClose(Info);
end;
\end{lstlisting}
Finally, the 'destroy' callback for the window is set, and the OK button's
'click' signal is attached to the destroy method of the window widget:
\begin{lstlisting}{}
gtk_signal_connect(PGtkObject(Window),'destroy',
TGTKSignalFunc(@DestroyPropDialog),Result);
gtk_signal_connect_object(PgtkObject(OKButton),'clicked',
GTK_SIGNAL_FUNC(@gtk_widget_destroy),
PGTKOBJECT(Window));
end;
end;
\end{lstlisting}
Showing the properties dialog is simple:
\begin{lstlisting}{}
Procedure ShowFilePropertiesDialog(Dialog : PFilePropertiesDialog);
begin
gtk_widget_show_all(PgtkWidget(Dialog^.Window));
end;
\end{lstlisting}
The result of all this is shown in figure \ref{fig:fileprops}.
\begin{figure}[ht]
\begin{center}
\caption{The file properties dialog.}\label{fig:fileprops}
\epsfig{file=gtk4ex/fileprops.png,width=8cm}
\end{center}
\end{figure}
The handling of the mask form is a little bit more complicated than the
properties dialog, since the mask form should return some information
to the main form.
The creation of the mask form is again a standard matter, and the reader
is referred to the code on the CD-ROM to see how it is handled. The
only thing worth noting is the handling of the click on the 'OK' button that
appears on the form.
\begin{lstlisting}{}
gtk_signal_connect(PgtkObject(OKButton),'clicked',
TGtkSignalFunc(@ApplyMask),Result);
gtk_signal_connect_object(PgtkObject(OKButton),'clicked',
GTK_SIGNAL_FUNC(@gtk_widget_destroy),
PGTKOBJECT(Window));
\end{lstlisting}
Two handlers are added to the 'clicked' signal of the 'OK' button.
The first one is pointed to a function that will apply the mask, and the
second one is redirected to the destroy method of the dialog window wigdet.
\begin{lstlisting}{}
Procedure ApplyMask(Widget : PGtkWidget; Window : PMaskForm);cdecl;
begin
With Window^ do
begin
Mask:=StrPas(gtk_entry_get_text(EMask));
If (CallBack<>Nil) then
CallBack(Mask,CallBackData);
end;
end;
\end{lstlisting}
The \lstinline|TMaskForm| record that contains fields for all widgets on
the mask entry form also contains 2 fields that allow the OK button to notify
the calling program of the new mask:
\begin{lstlisting}{}
TMaskCallBack = Procedure (Mask : String; Data : Pointer);
TMaskForm = Record
{ ... widget fields ... }
Mask : ShortString;
CallBack : TMaskCallBack;
CallBackData : Pointer;
end;
PMaskForm = ^TMaskForm;
\end{lstlisting}
If the callback field is set, then the \lstinline|ApplyMask| function will call
it and pass it the new mask and some arbitrary pointer.
The main form contains a 'file mask' menu item, which has the following
'click' handler:
\begin{lstlisting}{}
procedure DoMask(Widget : PGtkWidget ; MainForm : PMainWindow ); cdecl;
Var
S : AnsiString;
begin
With NewMaskForm^ do
begin
S:=MainForm^.FMask;
gtk_entry_set_text(EMask,PChar(S));
CallBack:=@ApplyMask;
CallBackData:=MainForm;
gtk_widget_show_all(PgtkWidget(Window));
end;
end;
\end{lstlisting}
When the user clicks the 'file mask' menu item, A mask entry form is created.
The current file mask is filled in the entry widget (\lstinline|EMask|).
The callback is set, and the callbackdata is set to the pointer to the main
window record. The callback that is executed when the user clicks the OK
button on the mask form is the following:
\begin{lstlisting}{}
Procedure ApplyMask(Mask : String; Data : Pointer);
begin
PMainWindow(data)^.FMask:=Mask;
RefreshFileView(PMainWindow(Data));
end;
\end{lstlisting}
The reason that this system of callbacks is needed is that the
\lstinline|gtk_widget_show_all| immediatly returns when the mask entry form is
shown. Even though the mask entry form dialog is a modal dialog (i.e. it alone will
respond to mouse clicks and key presses) the call returns immediatly,
there is no counterpart for the Delphi \lstinline|ShowModal| function.
When the \lstinline|gtk_widget_show_all| returns, the mask entry form is still on
the screen, so the changes made in the mask form must be communicated
back to the main form by means of a callback which is executed when
the mask entry form is closed.
The mask form in action is shown in figure \ref{fig:filemask}.
\begin{figure}[ht]
\begin{center}
\caption{The file properties dialog.}\label{fig:filemask}
\epsfig{file=gtk4ex/filemask.png,width=8cm}
\end{center}
\end{figure}
\section{Finishing the application}
In several places in this article, a reference was made to the main menu.
The main menu is created in the \lstinline|NewMainMenu| function; since
menus were discussed extensively in the previous article on programming GTK,
the code will not be presented here. The various calls developed in the
previous article have been collected in the \file{menus} unit. One
additional call was added which adds a check menuitem to a menu; the call is
similar to the regular menu item calls, and will not be discussed here.
The application is built in such a way that it can easily be extended.
Only 2 file actions have been implemented, but many more can be made.
Missing functionality includes:
\begin{itemize}
\item Renaming of files. The CList allows to put an arbitrary widget into
a cell; this functionality could be used to allow the user to change the
filename by simply editing it.
\item Moving and copying of files, using drag and drop.
\item Duplicating the main window, or spawning a new window.
\item Opening a file in another application.
\item Improve the look of the file properties form.
\item On Windows, support for showing different drives should be added.
\end{itemize}
And without doubt, many more can be found.
\end{document}