The DK libraries for Java provide classes for both GUI and non-GUI related problems:
The 3-argument constructor of DkTool does the following steps:
The openResource method searches for a file in the following locations:
The openLocalizedResource() method attempts the language/region and language subdirectories in each of the directories listed above first.
When specifying a resource base or the configuration directory I
strongly recommend to use a "http://" or "file://" prefix.
Names without the prefix are interpreted as a path in the *.jar
file.
To open streams for resources in the *.jar file we use
xyz.getClass().getClassLoader().getResourceAsStream(abc) so
you must place resources in a correct path relative to the objects
class file.
If your language is not English and you want to compile your software on both UTF-8 and non-UTF-8 enabled systems I suggest to use English strings in the source and to provide strings in your native language using localization.
You should pass the first command line argument (if any) as
third argument to the DkTool constructor which uses it as
configuration directory and first resource directory. Like all
resource directories the directory should have a prefix "file://"
(for local applications) or "http://".
Remember: Locations without "file://" or "http://" are
interpreted as paths in the *.jar file.
One of the activities in the DkTool constructor is to search for
a file "config.txt", which can be either in one of the resource
bases (preferred) or in the *.jar file.
This file consists of a series of sections, each section consists
of a scope part and a settings part.
The scope part consists of
"[user/host/application]",
"[user/host]" and "[user]" lines. The wildcard
"*" represents all users/all hosts/all applications (depending on
the position).
The setting part consists of key/value pairs, one per line, key and
value separated by "=".
Example:
[*] resources.directory.fonts=file:///usr/local/gs-fonts [*/*/UselessForm] resources.directory.application=file:///home/joe/exuf
In the example we set the property "resources.directory.fonts"
to "file:///usr/local/gs-fonts" for all users on all hosts running
any application.
For application "UselessForm" run by any user on any host we set
"resources.directory.application" to "file:///home/joe/exuf".
The most important property to set is
"resources.directory.application" for each application.
The application searches for resources (i.e. files containing
localized texts) in this directory.
The properties "resources.directory.fonts" and
"resources.directory.images" are used by objects implementing
java.awt.print.Printable created by fig2vect to search for fonts
and embedded images.
Summary: In the configuration directory there must be a file "config.txt" containing at least:
[*] resources.directory.fonts=... [*/*/...] resources.directory.application=...
A String array of localized texts can be read using DkTool's
getLocalizedArray() method. The arguments are an object
reference and a file name. The file name is used in a resource
search, the file is read, each non-comment line is used to build
one of the strings in the returned array.
The file for localized texts consists of text lines and comment
lines. A comment line is started by a raute '#' as first character
in the line, all other lines are text lines (event empty lines,
empty text lines result in an emty text).
I suggest to use comments for indices and the purpose of a
text.
Note: The file must be UTF-8 encoded. I recommend to use
vim or gedit on a Linux system with
LANG=language_region.UTF-8 setting. There may be
other editors on other systems allowing to save files using UTF-8
encoding too.
In the Java application code the programmer specifies a file
name without the leading directory.
You have to save the localized texts in a file
language/region/filename or
language/filename in one of the resource base
directories in the resource base list.
In this example we will create a useless form application and
show how to use the methods from DkTool and DkFrame.
A user can specify surname and name and his/her opinion about the
form and create a printout of the entered data.
We will show how to use preferences and localization. You will also
see how to create a Printable object from a *.fig drawing and how
to send it to the printer.
UselessFormMain.java | The main program. |
UselessWindow.java | The programs main window, the class is derived from DkFrame. |
UselessWindow.jl | Description of the contents of UselessWindow. This file is merged into UselessWindow.java by the jlayout program. |
UselessPrintable.java | Implements java.awt.print.Printable. This file is created from certificate.fig using the fig2vect program. |
certificate.fig | The printout, the pattern is created in XFig, jFig, WinFig or any other program producing a *.fig file. Remember not to use fill patterns on splines! |
We want to create a GUI, this should be done from within the AWT thread. We create a class GuiCreator implementing the Runnable interface and pass an object of this class to SwingUtilities.invokeLater().
class GuiCreator implements Runnable { private String dir = null; private String[] def_t = null; public GuiCreator(String s, String[] t) { } public void run() { } }
The constructor of GuiCreator uses two arguments: a
configuration directory name (may be null) and an array containing
default values for all the texts or strings used in the
application.
Both arguments are saved for later use.
class GuiCreator implements Runnable { private String dir = null; private String[] def_t = null; public GuiCreator(String s, String[] t) { dir = s; def_t = t; } public void run() { } }
The GuiCreator.run() method is run from the AWT thread. First we
create a DkTool object, we pass the application name, an
application group name and the directory saved by the constructor
to the new object.
The constructor of the DkGuiTool object uses dkt to retrieve
properties and preferences for GUI setup. It establishes a look and
feel, enforces minimum font sizes and can replace a font family by
another one.
We attempt to retrieve an array containing localized texts for our
application from a file "useless.txt". If such an array is found it
will be used in the further steps, otherwise we use the default
texts in def_t.
The UselessWindow class is the main window class of our project. We
pass dkt, dkg and either def_t or loc_t to the constructor. After
packing the window we can attempt to restore position and size
stored by a previous run of the program before we make the window
visible. If there is no stored information the window will appear
centered on the screen.
class GuiCreator implements Runnable { private String dir = null; private String[] def_t = null; public GuiCreator(String s, String[] t) { dir = s; def_t = t; } public void run() { DkTool dkt = new DkTool(def_t[0], def_t[1], dir); DkGuiTool dkg = new DkGuiTool(dkt); String[] loc_t = dkt.getLocalizedArray(this, "useless.txt"); String[] t = def_t; if(loc_t != null) { if(loc_t.length >= def_t.length) t = loc_t; } UselessWindow w = new UselessWindow(dkt, dkg, t); w.pack(); w.choosePosition(); w.setVisible(true); } }
The def_t array in the UselessFormMain class contains the
default values for all texts used in the application. This array
will grow while we continue to work on the project.
It is a good idea to have comments showing the index before each
array element and a comment about the use of each element at the
line end.
public static final String[] def_t = { /* 0 */ "UselessForm", // Application name /* 1 */ "NonsensApplications", // Application group name /* 2 */ "Surname:", // GUI elements /* 3 */ "Name:", /* 4 */ "Opinion:", /* 5 */ "Very good", /* 6 */ "Useless", /* 7 */ "Waste of time", /* 8 */ "Print", /* 9 */ "Exit", /* 10 */ "Please enter your surname here.", // Tool-tip texts /* 11 */ "Please enter your name here.", /* 12 */ "Please specify your opinion about this program here.", /* 13 */ "Print certificate about the filled form.", /* 14 */ "Exit this application.", /* 15 */ "Error", // Error message /* 16 */ "Please fill all the text fields.",
Finally we have the entire file here:
package uselessform; import javax.swing.SwingUtilities; import dirk_krause.tools.*; class GuiCreator implements Runnable { private String dir = null; private String[] def_t = null; public GuiCreator(String s, String[] t) { dir = s; def_t = t; } public void run() { DkTool dkt = new DkTool(def_t[0], def_t[1], dir); DkGuiTool dkg = new DkGuiTool(dkt); String[] loc_t = dkt.getLocalizedArray(this, "useless.txt"); String[] t = def_t; if(loc_t != null) { if(loc_t.length >= def_t.length) t = loc_t; } UselessWindow w = new UselessWindow(dkt, dkg, t); w.pack(); w.choosePosition(); w.setVisible(true); } } /** * * @author krause */ public class UselessFormMain { /** Texts used in the GUI. */ public static final String[] def_t = { /* 0 */ "UselessForm", // Application name /* 1 */ "NonsensApplications", // Application group name /* 2 */ "Surname:", // GUI elements /* 3 */ "Name:", /* 4 */ "Opinion:", /* 5 */ "Very good", /* 6 */ "Useless", /* 7 */ "Waste of time", /* 8 */ "Print", /* 9 */ "Exit", /* 10 */ "Please enter your surname here.", // Tool-tip texts /* 11 */ "Please enter your name here.", /* 12 */ "Please specify your opinion about this program here.", /* 13 */ "Print certificate about the filled form.", /* 14 */ "Exit this application.", /* 15 */ "Error", // Error message /* 16 */ "Please fill all the text fields.", }; /** * @param args the command line arguments */ public static void main(String[] args) { String dir = null; if(args.length > 0) { dir = args[0]; } SwingUtilities.invokeLater(new GuiCreator(dir, def_t)); } }
We will use the following variable names for the GUI
elements:
lSurname | Label before the surname text field. |
tfSurname | Surname text field |
lName | Label before the name text field. |
tfName | Text field for the name. |
lOpinion | Label before the opinion combo box. |
cbOpinion | Opinion combo box |
bPrint | Print button. |
bExit | Exit button. |
The main window class is derived from DkFrame. So the first
action of the constructor is to call the DkFrame constructor, the
window title, ad DkTool and a DkGuiTool are passed to the DkFrame
constructor.
The DkTool object and the DkGuiTool object are saved to the
variables dkt and dkgt by the DkFrame constructor.
Both variables are protected, so they are available to our classes
methods.
public UselessWindow(DkTool d, DkGuiTool g, String[] new_t) { super(new_t[0], d, g); ... }
The texts provided by the GuiCreator are saved in an internal variable String[] t.
private String[] t = null; public UselessWindow(DkTool d, DkGuiTool g, String[] new_t) { super(new_t[0], d, g); t = new_t; ... }
For the "Opinion:" combo box we need an array of the three choices.
public UselessWindow(DkTool d, DkGuiTool g, String[] new_t) { super(new_t[0], d, g); t = new_t; cbValues = new String[3]; cbValues[0] = t[5]; cbValues[1] = t[6]; cbValues[2] = t[7]; ... }
Now we call jLayoutSetup(). This method does not yet exist, the jlayout program will generate it.
public UselessWindow(DkTool d, DkGuiTool g, String[] new_t) { super(new_t[0], d, g); t = new_t; cbValues = new String[3]; cbValues[0] = t[5]; cbValues[1] = t[6]; cbValues[2] = t[7]; jLayoutSetup(); ... }
The final action in the constructor is to check whether settings were saved in a previous run of the program. If we find settings we want to restore them.
private String[] cbValues = null; public UselessWindow(DkTool d, DkGuiTool g, String[] new_t) { super(new_t[0], d, g); t = new_t; cbValues = new String[3]; cbValues[0] = t[5]; cbValues[1] = t[6]; cbValues[2] = t[7]; jLayoutSetup(); restoreTexts(); }
We use names "uselessform.surname"... for the preferences. The
result array values and ivalues are initialized with
useful default which can be used if no previous settings are
found.
After retrieving the preferences we check the ivalues[0]
value and correct it if necessary to avoit out-of-bounds
exceptions. If the values[] elements are not null they can
be used as initial texts for the text fields.
private static final String[] ufPkStr = { "uselessform.surname", "uselessform.name", }; private static final String[] ufPkInt = { "uselesform.opinion", }; private void restoreTexts() { String[] values = { null, null }; int[] ivalues = { 0 }; dkt.getMultiplePreferenceStrings(ufPkStr, values); dkt.getMultiplePreferenceInts(ufPkInt, ivalues); if(ivalues[0] < 0) { ivalues[0] = 0; } if(ivalues[0] >= cbValues.length) { ivalues[0] = cbValues.length - 1; } if(values[0] != null) { tfSurname.setText(values[0]); } if(values[1] != null) { tfName.setText(values[1]); } cbOpinion.setSelectedIndex(ivalues[0]); }
Each time a window inherited from DkFrame is closed the method cleanup() is invoked. The isLast parameter is true for the last window closed. This is the right time to save settings.
@Override public void cleanup(boolean isLast) { if(isLast) { saveTexts(); } jLayoutCleanup(); }
To save the settings we collect the text field texts in an array and create an array (array size 1) for the int value. After saving the preferences to the DkTool object
private void saveTexts() { String[] values = { null, null }; int[] ivalues = { 0 }; values[0] = tfSurname.getText(); values[1] = tfName.getText(); ivalues[0] = cbOpinion.getSelectedIndex(); dkt.setMultiplePreferenceStrings(ufPkStr, values); dkt.setMultiplePreferenceInts(ufPkInt, ivalues); dkt.flushPreferences(); }
The jlayout program reads a *.jl file describing the GUI and adds four sections to the *.java source:
The places where to add which section are marked up by special comment pairs.
package uselessform; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowEvent; import dirk_krause.tools.*; // jlayout.import.begin // jlayout.import.end /** * * @author krause */ public class UselessWindow extends DkFrame implements ActionListener { ... // jlayout.variables.begin // jlayout.variables.end // jlayout.setup.begin // jlayout.setup.end // jlayout.cleanup.begin // jlayout.cleanup.end ... }
package uselessform; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowEvent; import dirk_krause.tools.*; // jlayout.import.begin // jlayout.import.end /** * * @author krause */ public class UselessWindow extends DkFrame implements ActionListener { private String[] t = null; private String[] cbValues = null; private static final String[] ufPkStr = { "uselessform.surname", "uselessform.name", }; private static final String[] ufPkInt = { "uselesform.opinion", }; // jlayout.variables.begin // jlayout.variables.end // jlayout.setup.begin // jlayout.setup.end // jlayout.cleanup.begin // jlayout.cleanup.end public UselessWindow(DkTool d, DkGuiTool g, String[] new_t) { super(new_t[0], d, g); t = new_t; cbValues = new String[3]; cbValues[0] = t[5]; cbValues[1] = t[6]; cbValues[2] = t[7]; jLayoutSetup(); restoreTexts(); } private void saveTexts() { String[] values = { null, null }; int[] ivalues = { 0 }; values[0] = tfSurname.getText(); values[1] = tfName.getText(); ivalues[0] = cbOpinion.getSelectedIndex(); dkt.setMultiplePreferenceStrings(ufPkStr, values); dkt.setMultiplePreferenceInts(ufPkInt, ivalues); dkt.flushPreferences(); } private void restoreTexts() { String[] values = { null, null }; int[] ivalues = { 0 }; dkt.getMultiplePreferenceStrings(ufPkStr, values); dkt.getMultiplePreferenceInts(ufPkInt, ivalues); if(ivalues[0] < 0) { ivalues[0] = 0; } if(ivalues[0] >= cbValues.length) { ivalues[0] = cbValues.length - 1; } if(values[0] != null) { tfSurname.setText(values[0]); } if(values[1] != null) { tfName.setText(values[1]); } cbOpinion.setSelectedIndex(ivalues[0]); } private boolean checkString(String s) { boolean back = false; if(s != null) { if(s.length() > 0) { back = true; } } return back; } private void attemptToPrint() { String surName = dkt.trimString(tfSurname.getText()); String name = dkt.trimString(tfName.getText()); int i = cbOpinion.getSelectedIndex(); if(checkString(surName)) { if(checkString(name)) { UselessPrintable up = new UselessPrintable(dkt); up.replaceText("Person", surName + " " + name); up.replaceText("Opinion", cbValues[i]); up.replaceText("01.01.2000", DkTool.getCurrentDateGerman()); sendToPrinter(up); } else { tfName.grabFocus(); msgError(t[15], t[16], false); } } else { tfSurname.grabFocus(); msgError(t[15], t[16], false); } } int[] keywordsToCheck = { 8, // Print 9, // Exit }; @Override public void actionPerformed(ActionEvent e) { String ac = e.getActionCommand(); int a = DkTool.getStringArrayIndex(t, ac, keywordsToCheck); switch(a) { case 8: { attemptToPrint(); } break; case 9: { processWindowEvent( new WindowEvent(this, WindowEvent.WINDOW_CLOSING) ); } break; } } @Override public void cleanup(boolean isLast) { if(isLast) { saveTexts(); } jLayoutCleanup(); } }
The UselessWindow.jl file must be placed in the same directory as the UselessWindow.java file.
At the beginning of the file we have some options and default settings:
.option use tool tip text as accessible description = yes .default border = border .default fill = none .default anchor = west
Next we are dealing with the features of the main window itself:
border = border fill = none title = t[0] default close operation = dispose layout = gridbag contents = paContents 0 0 1 1
The paContents element is of type JPanel. It uses a BoxLayout
along the page axis (elements go from top to bottom).
Note: In difference to classical programs
elements/variables are used before they are declared.
The paContents panel contains the panel for the input elements and
another panel for the buttons.
[JPanel paContents] layout = box.page_axis contents = pInput contents = pButtons
In the pInput panel the elements are organized in a gridbag
layout again. The lSurname label is placed in column 0, row
0 (left top) and is one column wide and one row high.
The tfSurname is placed one column right beside the previous
element (+1) in the same row (.). The element is also one column
wide and one row high.
The next element lName starts in column 0 (0) on the next
row (+1)... (I hope you got it).
[JPanel pInput] layout = gridbag contents = lSurname 0 0 1 1 contents = tfSurname +1 . 1 1 contents = lName 0 +1 1 1 contents = tfName +1 . 1 1 contents = lOpinion 0 +1 1 1 contents = cbOpinion +1 . 1 1
The text for the label lSurname is taken from t[2]. The label is right aligned (the anchor is east).
[JLabel lSurname] text = t[2] anchor = east
The text field tfSurname has a width of 20
characters.
The text for the tool tip is obtained from t[10].
[JTextField tfSurname] constructor = 20 tool tip text = t[10]
For a combo box we provide a text array as constructor argument. We created the array in the constructor of the main window.
[JComboBox cbOpinion] constructor = cbValues tool tip text = t[12]
The buttons panel uses a BoxLayout on a line (if possible all
elements are placed in one line).
We want to have glue (the translation from german to english would
result in: gum) between the panel borders and the buttons and
between the buttons. The keyword $glue is used for that. If
the panel grows (horizontally) the additional space is is used for
all the glues equally.
[JPanel pButtons] layout = box.line_axis contents = $glue contents = bPrint contents = $glue contents = bExit contents = $glue
The label and the action command text for the bPrint
button are obtained from t[8]. The action listener is the
main window itself (remember: the jLayoutSetup() function to
generate the GUI contents elements is a method of this class).
The bPrint and bExit button are set to equal size
because they are members of the same size group.
[JButton bPrint] text = t[8] tool tip text = t[13] action listener = this size group = buttons [JButton bExit] text = t[9] tool tip text = t[14] action listener = this size group = buttons
Here is the entire file:
.option use tool tip text as accessible description = yes .default border = border .default fill = none .default anchor = west border = border fill = none title = t[0] default close operation = dispose layout = gridbag contents = paContents 0 0 1 1 [JPanel paContents] layout = box.page_axis contents = pInput contents = pButtons [JPanel pInput] layout = gridbag contents = lSurname 0 0 1 1 contents = tfSurname +1 . 1 1 contents = lName 0 +1 1 1 contents = tfName +1 . 1 1 contents = lOpinion 0 +1 1 1 contents = cbOpinion +1 . 1 1 [JLabel lSurname] text = t[2] anchor = east [JTextField tfSurname] constructor = 20 tool tip text = t[10] [JLabel lName] text = t[3] anchor = east [JTextField tfName] constructor = 20 tool tip text = t[11] [JLabel lOpinion] text = t[4] anchor = east [JComboBox cbOpinion] constructor = cbValues tool tip text = t[12] [JPanel pButtons] layout = box.line_axis contents = $glue contents = bPrint contents = $glue contents = bExit contents = $glue [JButton bPrint] text = t[8] tool tip text = t[13] action listener = this size group = buttons [JButton bExit] text = t[9] tool tip text = t[14] action listener = this size group = buttons
Run
jlayout UselessWindow.jl UselessWindow.java
To merge the information from the *.jl file into the *.java
file.
When rewriting the *.java file jlayout keeps the comment pairs
marking up the section positions.
In an iterative development cycle you can change the *.jl file and
run jlayout again as often as necessary.
To print something we need an object implementing the
java.awt.print.Printable interface.
A large part of the printout is constant, only the name, the
opinion and the current date are varied by the program.
We prepare the printout as *.fig file first, use either XFig, jFig
or WinFig to create the file certificate.fig in a new subdirectory "fig" of
our project.
We use placeholders for the texts to vary:
Person | The surname and name entered in the form. |
Opinion | The opinion entered in the form. |
01.01.2000 | The current date. |
After finishing the *.fig file we run
fig2vect -ljava.a4-web -odklibsj=yes certificate.fig ../src/uselessform/UselessPrintable.java
The new class UselessPrintable appears in your Java IDE in the uselessform package.
At the beginning of the file you see a list of resources needed by the file, fonts and images. You can either pack the fonts and images into the *.jar file or make them available in resource directories specified as file:// or http:// URLs.
In the attemptToPrint() method of the UselessWindow class
you see that replaceText() is used to replace the
placeholders "Person", "Opinion" and "01.01.2000" by real data.
The sendToPrinter() method from the DkFrame class is used to
print the object.
In our example the main() function passes the first
command line argument to the constructor of the GuiCreator object
which in turn passes it to the DkTool constructor.
This third argument of the DkTool constructor is used as
configuration directory and as the first resource directory. In
your home directory create a subdirectory "/home/joe/conf".
In your IDE set up the first command line argument when running the
UselessForm application to "file:///home/joe/conf". Do not forget
the "file://" prefix.
The constructor of DkTool attempts to find a DkTool-property
file "config.txt". Typically this file is located in the
configuration directory.
It should contain three properties for each application:
[*] resources.directory.font=file:///usr/local/gs-fonts [*/*/UselessForm] resources.directory.application=file:///home/joe/exuf
Get the recent GhostScript source distribution and unpack it. Copy all the files from the Resources/Fonts directory to /usr/local/gs-fonts.
Now you should be able to run the application and print the useless form.
The file for localized texts consists of text lines and comment
lines. A comment line is started by a raute '#' as first character
in the line, all other lines are text lines (event empty lines,
empty text lines result in an emty text).
I suggest to use comments for indices and the purpose of a
text.
# # 0: Application name UselessForm # # 1: Application group name NonsensApplications # # 2: Label: Surname Vorname: # # 3: Label: Name Name: # # 4: Label: Opinion Meinung: # # 5: Combo box value: Opinion = Very good Toll # # 6: Combo box value: Opinion = Useless Nutzlos # # 7: Combo box value: Opinion = Waste of Time Zeitverschwendung # # 8: Button text: Print Drucken # # 9: Button text: Exit Beenden # # 10: Tool tip text: Enter your surname here Bitte geben Sie hier Ihren Vornamen an. # # 11: Tool tip text: Enter your name here Bitte geben Sie hier Ihren Namen an. # # 12: Tool tip text: Specify your opinion Bitte geben Sie Ihre Meinung zu diesem Formular an. # # 13: Tool tip text: Print certificate Zertifikat ausdrucken. # # 14: Tool tip text: Exit this application Diese Anwendung verlassen. # # 15: Error message box title Fehler # # 16: Error text: Please fill all the fields Bitte füllen Sie alle Felder aus.
Note: Please leave texts 0 and 1 as in the original.
In /home/joe/exuf create a subdirectory "de" and place the file in this directory.
Now you should be able to run the application. If your language
setting (LANG environment variable) is "de" or "de_DE" or
"de_DE.UTF-8" you should see the GUI in german language.
The printout is in english language as we did not make any attempt
to localize it. To localize the printout we would need to create a
Printable or each language we want to support...
The localized texts for favorite languages (i.e. English and
your native language) should be included to the *.jar file.
The text file should be included in a "language" or
"language/region" subdirectory in the *.jar file,
language and region are the two-letter codes for your
country and region converted to lower case. I.e. a german version
of useless.txt can be placed in a "de" subdirectory.
The way to insert additional files into a *.jar file depends on the
IDE you use.
If you use NetBeans and the useless form project is placed in
/home/joe/NetBeansProjects/UselessForm, create a directory
/home/joe/NetBeansProjects/UselessForm/build/classes/de and place
useless.txt in this directory. Force the IDE to rebuild the *.jar
file by adding a useless space to one of the *.java files, remove
the space and save the file.