-
Notifications
You must be signed in to change notification settings - Fork 9
GUI Basics
This page is to give you an overview of how to build a simple GUI to observe the progress, and potentially control your experiment.
In general, GUIs can be fairly complicated to make from scratch. Java provides a rather good set of libraries for creating GUIs in the form of "JavaFX". There will be a tutorial on how to create your own GUI from scratch using JavaFX later, for those interested.
However, if you're not feeling up to that, you can build up a basic GUI using JISA
. This revolves around using pre-defined "blocks" that you can then stick together in a tiled formation, or have them appear in individual windows. These blocks mostly centre around displaying the contents of a ResultList
object so if you haven't done so already, take a look at the Result Handling Basics page.
This page serves as a tutorial for using these pre-defined GUI blocks.
Note that data shown in these examples is all completely ficticious, mostly radomly generated since I didn't have any instruments connected to perform actual measurements.If you want to use GUI elements in your application you will need to first make a small change to you Main
class. Specifically you will need to make it extend GUI
, so that it now looks like:
public class Main extends GUI {
public static void run() throws Exception {
}
public static void main() {
try {
run();
} catch (Exception e) {
Util.exceptionHandler(e);
}
}
}
The reason for this that GUIs operate on a different set of principles to command-line driven programs. They have to keep running waiting for the user to do something (like click a button) and not just quit when it has reached the end of whatever code it is currently running. By extending GUI
we ensure that the program keeps running so long as there is a window open.
Alternatively you can start and stop the GUI thread manually like so:
public class Main {
public static void run() throws Exception {
GUI.startGUI();
// Code goes here
GUI.stopGUI();
}
public static void main() {
try {
run();
} catch (Exception e) {
Util.exceptionHandler(e);
this way, the GUI thread will keep going until GUI.stopGUI()
is called. Trying to use any GUI elements before GUI.startGUI()
is called or after GUI.stopGUI()
will cause an exception.
If you wish to use these GUI elements in Python, then the manual start and stop methods are your only choice.
from JISA.GUI import GUI;
def main():
GUI.startGUI();
GUI.stopGUI();
main();
The simplest of these GUI elements is Table
. This is a window that will display the contents of a ResultList
object in real-time as a table. Let's say we have a ResultList
object called list
,
ResultList list = new ResultList("Frequency", "Voltage", "Current");
list.setUnits("Hz", "V", "A");
then we can create a Table
to display its contents like so:
Table table = new Table("My Results", list);
Now that the Table
is created, we can make it show itself by use of show()
:
table.show();
then to temporarily hide it:
table.hide();
and to permanently close it:
table.close();
The result looks like this:
This table updates automatically every time something new is added to our ResultList
, so it gives us a real-time view of our results.
When clear()
is called on the ResultList
object it is watching, the Table
will also clear:
list.clear();
If you want to clear the Table
only (ie keep the data in the ResultList
):
table.clear();
Perhaps the coolest of them all is the plot element. This will plot one column of the ResultList
against another, displaying updates in real-time with some nifty animations.
Let's assume the same ResultList
as before:
ResultList list = new ResultList("Frequency", "Voltage", "Current");
list.setUnits("Hz", "V", "A");
Then if we do
Plot plot = new Plot("My Plot Title", list);
plot.show();
like with Table
we will get a plot of the first two columns (ie Frequency on the x-axis and Voltage on the y-axis). To specify different columns you just need to provide their column number. For example, to plot Current against Voltage:
Plot plot = new Plot("My Plot Title", list, 1, 2);
This results in a plot like this:
Alternatively, if you want to plot multiple things, you can create the Plot
like so:
Plot plot = new Plot("My Plot Title", "X-Axis Label", "Y-Axis Label");
Then add each data series manually:
plot.watchList(list, 1, 2, "Current", Color.RED);
plot.watchList(list, 1, 0, "Frequency", Color.BLUE);
This will tell the Plot
to plot Current vs Voltage in red labelled as "Current" in the legend, and Frequency vs Voltage in blue labelled as "Frequency" in the legend.
The same principles apply to Plot
objects as do to Table
objects when it comes to clearing:
list.clear(); // Clears the data and the plot
plot.clear(); // Clears the plot only
In many cases, it is useful to display the current progress of an operation by use of a progress bar. This is easily implemented using JISA by use of the Progress
class. Simple create a Progress
object and show()
it like so:
Progress bar = new Progress("Title Goes Here");
bar.show();
then at any point you can update its value like so:
bar.setProgress( value, maxValue );
You can also change the title at any time by use of:
bar.setTitle("New Title");
and add an extra message to show underneath the bar (ie to tell the user what is currently happening) by use of:
bar.setStatus("Measuing voltage...");
To see this, consider the following code:
Progress bar = new Progress("Current Progress");
bar.setStatus("This is a demo");
bar.setProgress(0,10);
bar.show();
This results in
Code | Result |
---|---|
bar.setProgress(1,10); |
|
bar.setProgress(5,10);
bar.setStatus("Half way there!"); |
|
bar.setProgress(10,10);
bar.setStatus("All done!"); |
If you have multiple GUI elements, it can be annoying to have each in a separate window. To remedy this, JISA provides the Grid
class. This will take other GUI elements at display them in a signal window, in a tiled formation. This is done very easily like so:
// Create list to store results
ResultList list = new ResultList("Frequency", "Voltage", "Current");
list.setUnits("Hz", "V", "A");
// Create GUI elements, but don't show() them yet
Table table = new Table("Results", list);
Plot plot = new Plot("Results", list, 1, 2);
Progress prog = new Progress("Experiment Progress");
// Create a grid, adding the elements in order
Grid grid = new Grid("Experiment Status", prog, table, plot);
// Show the grid to show all the elements in the grid
grid.show();
The results of which looks like:
A Grid
is also a good way of creating a GUI to control your experiment. You can add bottons to the top of the grid in a toolbar by using the addToolbarButton(...)
method. Clearly you will need to define what you want the program to do when the button is clicked. This can be done in a couple of different ways. The first is to directly write the code you want to run when clicked like so:
grid.addToolbarButton("Button Text", () -> {
// Code to run when button is clicked goes here
});
(the () -> {}
is called a lambda expression)
Alternatively, if you have a function defined somewhere and you want it to run that when clicked you can simply write its name (including class name) like so:
public class Main extends GUI {
public static void run() {
...
grid.addToolbarButton("Button Text", Main::doTheThing);
}
public static void doTheThing() {
// The thing is done here
}
...
}
The end result looks like this:
As an example, let's look at how we'd go about making a GUI much like the one above that only causes the experiment to start when the button is pressed:
public class Main extends GUI {
// Declare variable up here so that it's accessible in all methods
static ResultList list;
static Progress bar;
static Table table;
static Plot plot;
static Grid grid;
static SR830 lockIn;
static K2450 smu;
public static void run() throws Exception {
lockIn = new SR830(new GPIBAddress(0,30));
smu = new K2450(new GPIBAddress(0,15));
list = new ResultList("Frequency", "Voltage", "Current");
list.setUnits("Hz", "V", "A");
bar = new Progress("Experiment Progress");
table = new Table("Results", list);
plot = new Plot("Plot of Results", list, 1, 2);
grid = new Grid("Experiment Control", bar, table, plot);
grid.addToolbarButton("Start", Main::startExperiment);
grid.show();
}
public static void startExperiment() throws Exception {
// Take 10 readings
for (int i = 0; i < 10; i++) {
list.addData(
lockIn.getFrequency(),
smu.getVoltage(),
smu.getCurrent()
);
bar.setProgress(i+1,10);
}
}
public static void main() {
try {
run();
} catch (Exception e) {
Util.exceptionHandler(e);
}
}
}
Before pressing the button:
After pressing the button:
It is reasonable to assume that your program might require some user-input. To achieve this, the Fields
GUI element can be used. To a Fields
element you can add (the types of variable they return are shown in brackets):
- Text inputs (
String
) - Integer inputs (
int
) - Double inputs (
double
) - File selectors (
String
) - Check boxes (
boolean
) - Drop down / choice boxes (
int
)
Creating a Fields
object is done like this:
// Create the fields object
Fields inputs = new Fields("Title Goes Here");
// Add the fields
Field<String> name = inputs.addTextField("Name");
Field<Integer> age = inputs.addIntegerField("Age");
Field<Double> height = inputs.addDoubleField("Height (cm)");
Field<String> save = inputs.addFileSave("Write data to");
Field<String> open = inputs.addFileOpen("Get data from");
// Show the window
inputs.show();
This results in the following:
Clicking on a "browse" button will present the user with a file save or open dialogue (depending on whether we used addFileSave(...)
or addFileOpen(...)
respectively). For example, on a Linux (Gnome) system, the "Write data to" one will give us:
You should notice that each time we added a field, we got a Field<...>
object in return. The name between the <...>
indicates what type of value we expect the field to hold (ie addTextField will hold strings, addIntegerField will hold integers). These are our handle onto these fields and allow us to retrieve what is currently written in their respective field or set the value ourselves.
Field
objects provide four methods, the two most important of which are: set(...)
and get()
. set(...)
lets you set the value of whichever input the Field
is representing, whereas get()
will return the value currently stored in the field.
For example, let's say the user has typed in their info:
We can then retrieve what they have written by use of get()
:
String userName = name.get();
int userAge = age.get();
double userHeight = height.get();
String savePath = save.get();
String openPath = open.get();
which would be the equivalent of:
String userName = "Bob Smith";
int userAge = 34;
double userHeight = 175.4;
String savePath = "/home/william/Desktop/test.txt";
String openPath = "/home/william/data.txt";
If needed you can use set()
to set the value of the fields too:
name.set("Bary Merry");
age.set(73);
height.set(150.64);
save.set("/home/william/file.txt");
open.set("/usr/share/file.csv");
which will result in:
Field
objects also allow you to define what should happen when the contents of an input field is changed, like so:
Field<String> name = inputs.addTextField("Name");
name.setOnChange(() -> {
System.out.printf("Name changed to: %s\n", name.get());
});
Now, whenever the text in the "Name" text-box is changed, the new value is output to the terminal. For example, if it is empty and we type in "Bob", the terminal output will look like this:
Name changed to: B
Name changed to: Bo
Name changed to: Bob
You can add buttons, like the one in the above example, to the Fields
container by use of addButton(...)
, specifying text for the button and what to do when clicked like so:
input.addButton("Accept", () -> {
System.out.printf("Name: %s\n", name.get());
System.out.printf("Age: %d\n", age.get());
System.out.printf("Height: %f\n", height.get());
System.out.printf("Save: %s\n", save.get());
System.out.printf("Open: %s\n", open.get());
});
or
public class Main {
static Field<String> name;
static Field<Integer> age;
static Field<Double> height;
static Field<String> save;
static Field<String> open;
public static void main(String[] args) {
GUI.startGUI();
// Create the fields object
Fields inputs = new Fields("Title Goes Here");
// Add the fields
name = inputs.addTextField("Name");
age = inputs.addIntegerField("Age");
height = inputs.addDoubleField("Height (cm)");
save = inputs.addFileSave("Write data to");
open = inputs.addFileOpen("Get data from");
inputs.addButton("Accept", Main::onClick);
// Show the window
inputs.show();
}
public static void onClick() {
System.out.printf("Name: %s\n", name.get());
System.out.printf("Age: %d\n", age.get());
System.out.printf("Height: %f\n", height.get());
System.out.printf("Save: %s\n", save.get());
System.out.printf("Open: %s\n", open.get());
}
}
Clicking "Accept" will now cause the following to be output to the terminal:
Name: Bary Merry
Age: 73
Height: 150.640000
Save: /home/william/file.txt
Open: /usr/share/file.csv
Just with all the other GUI blocks, you can add a Fields
to a Grid
to make a cohesive GUI. Below is an example:
public class Main {
static double min = 0;
static double max = 20;
static int steps = 10;
static SMU smu;
static ResultList list;
public static void main(String[] args) throws Exception {
smu = new K2450(new GPIBAddress(0, 15));
list = smu.createSweepList();
// Start the GUI thread
GUI.startGUI();
// Create the GUI blocks
Fields inputs = new Fields("Experiment Parameters");
Table table = new Table("Table of Results", list);
Plot plot = new Plot("Plot of Results", list);
// Add the fields to the Fields block
Field<Double> minV = inputs.addDoubleField("Min Voltage [V]");
Field<Double> maxV = inputs.addDoubleField("Max Voltage [V]");
Field<Integer> step = inputs.addIntegerField("No. Steps");
// Set default values
minV.set(min);
maxV.set(max);
step.set(steps);
// Add button the Fields block
inputs.addButton("Set", () -> {
saveParams(minV.get(), maxV.get(), step.get());
});
// Create grid out of blocks
Grid grid = new Grid("Experiment", inputs, table, plot);
// Add button to grid toolbar
grid.addToolbarButton("Start", Main::startMeasurements);
// Show the grid
grid.show();
}
public static void saveParams(double minV, double maxV, int nSteps) {
min = minV;
max = maxV;
steps = nSteps;
GUI.infoAlert("Experiment", "Success", "Parameters have been set.");
}
public static void startMeasurements() throws Exception {
smu.doLinearSweep(
SMU.Source.VOLTAGE,
min,
max,
steps,
500,
false,
list
);
}
}
The result is the following:
Then we can enter new sweep parameters in the left-most block and press "Set" to apply them resulting in the following message:
Then we can press the "Start" button to run the SMU sweep resulting in:
You can add a check-box by use of:
Field<Boolean> tick = inputs.addCheckBox("Name goes here");
To add a drop-down "choice box", use to following:
Field<Integer> choice = inputs.addChoice("Name", "Option 1", "Option 2", etc...);
When you use choice.get()
, it will return an integer representing which option is selected (ie 0
for "Option 1"
, 1
for "Option 2"
etc).
Additionally for choices, you can use the editValues()
method to change what options are available:
choice.editValues("Option A", "Option B", "Option C");
The above example will change our choice box to now offer the choices of "Option A", "Option B" and "Option C" instead of whatever the options were before.
You may find that you will want multiple GUI elements grouped into separate sections. This is where the Tabs
element comes into play. Much like Grid
, it takes other GUI elements (including Grid
elements) and sorts them into tabs. Each element you add will appear in its own tab bearing the title it was given when created.
For example, let's say we have two Grid
elements, the first holding a Table
and Plot
for displaying results and the second for configuring the experiment.
ResultTable list = new ResultList("Voltage", "Current");
list.setUnits("V", "A");
Table table = new Table("Table of Results", list);
Plot plot = new Plot("Plot of Results", list);
Fields params = new Fields("Parameters");
Fields output = new Fields("Output");
Field<Double> minVoltage = params.addDoubleField("Min Voltage [V]");
Field<Double> maxVoltage = params.addDoubleField("Max Voltage [V]");
Field<Integer> numVoltage = params.addIntegerField("No. Steps");
Field<String> outputFile = output.addFileSave("Output File");
Grid results = new Grid("Results", table, plot);
Grid config = new Grid("Configuration", params, output);
Then we can link both of these grids together into the same window, each as a separate tab, like so:
Tabs window = new Tabs("My Experiment", results, config);
window.show();
or
Tabs window = new Tabs("My Experiment");
window.addTab(results);
window.addTab(config);
window.show();
which will result in the following:
Fancy making your application work like a mid-2000s website with endless pop-ups? Well then, look no further! You can make a variety of different dialog alert boxes pop-up very simply by use of the GUI
class like so:
// Error message
GUI.errorAlert("Window Title", "Message Title", "Message Content");
// Information message
GUI.infoAlert("Window Title", "Message Title", "Message Content");
// Warning message
GUI.warningAlert("Window Title", "Message Title", "Message Content");
Each one of these will halt your code until the user presses "Okay".
You can also ask for confirmation with confirmWindow(...)
like so:
boolean result = GUI.confirmWindow("Window Title", "Message Title", "Message Content");
if (result) {
// User pressed "Okay"
} else {
// User pressed "Cancel"
}
Method | Result |
---|---|
GUI.errorAlert(...); |
|
GUI.infoAlert(...); |
|
GUI.warningAlert(...); |
|
GUI.confirmWindow(...); |
You can also use the GUI
class to create a browser window to select a VISA instrument like so:
InstrumentAddress address = GUI.browseVISA();
This will create a window that looks like:
Clicking on one of the Select >
buttons will return the VISA address object of that instrument. Clicking cancel
will cause it to return null
- Getting Started
- Object Orientation
- Choosing a Language
- Using JISA in Java
- Using JISA in Python
- Using JISA in Kotlin
- Exceptions
- Functions as Objects
- Instrument Basics
- SMUs
- Thermometers (and old TCs)
- PID and Temperature Controllers
- Lock-Ins
- Power Supplies
- Pre-Amplifiers
- Writing New Drivers