by Steve Donovan
IupLua is a cross-platform kit for creating GUI applications in Lua. There are particularly powerful facilities for getting user input that don't require complicated coding, so it is particularly good for utility scripts.
Attributes are an important concept in IUP. You set and get them
just like table fields, but they are different from fields in several crucial
ways. First, case is not significant, SIZE
is just as good as
size
(but try to be consistent!). Second, writing to a non-existent
attribute will not give you an error, so proof-read carefully. Third,
writing to an attribute often causes some action; e.g the visible
attribute of controls can be used to hide them. It is best to think of them as a
special kind of function call.
Functions which create IupLua objects (i.e. constructors) take
tables as arguments. Lua allows you to drop the usual parentheses in such a
case, but remember that something like iup.fill{}
is not the same
as iup.fill()
; it is actually short for iup.fill({})
.
A Lua table can contain an array-like part (just items separated by commas) and
a map-like part (attribute-value pairs); the convention is to put the array part
first, and separate the map part from it with a semicolon. (See
Attributes/Guide/IupLua in the Manual for a good discussion.)
All the examples presented here and some utilities can be found at the "misc" folder in the IupLua examples.
Even simple scripts need to give the user some feedback. Otherwise people get
anxious and start worrying if their files really have been backed up, for
example. This is easy in IUPLua, and takes exactly one line. Note that all IUP
scripts must at least have a require 'iuplua'
statement at the
begining:
require( "iuplua" )
iup.Message('YourApp','Finished Successfully!')
Of course, many operations require confirmation from the user.
iup.Alarm
is designed for this:
require( "iuplua" )
b = iup.Alarm("IupAlarm Example", "File not saved! Save it now?" ,"Yes" ,"No" ,"Cancel")
-- Shows a message for each selected button
if b == 1 then
iup.Message("Save file", "File saved sucessfully - leaving program")
elseif b == 2 then
iup.Message("Save file", "File not saved - leaving program anyway")
elseif b == 3 then
iup.Message("Save file", "Operation canceled")
end
Like iup.Message
, the first parameter appears in the title bar
of the dialog box, the second parameter appears above the buttons, but
iup.Alarm
allows you to specify a number of buttons. The return code will
then tell you which button has been pressed, starting at 1 (which is always the
Lua way.)
The most common thing an interactive script will require from a user is a
file, or set of files. For simple cases, iup.GetFile
will do the
job:
require( "iuplua" )
f, err = iup.GetFile("*.txt")
if err == 1 then
iup.Message("New file", f)
elseif err == 0 then
iup.Message("File already exists", f)
elseif err == -1 then
iup.Message("IupFileDlg", "Operation canceled")
end
This will present you with the standard Windows File Open dialog box, and allow you to either choose a filename, or cancel the operation. Notice that this function returns two values, the filename and a code. The code will tell you whether the file does not exist yet (if for instance you typed a new filename into the file dialog box.)
The simplest way of getting general text is to use iup.GetText
:
require 'iuplua'
res = iup.GetText("Give me your name","")
if res ~= "" then
iup.Message("Thanks!",res)
end
Using this dialog, you can enter as many lines as you like, and press OK.
A better option for asking for a single string is the very versatile
iup.GetParam
:
require( "iuplua" )
require( "iupluacontrols" )
res, name = iup.GetParam("Title", nil,
"Give your name: %s\n","")
iup.Message("Hello!",name)
This has two advantages over plain GetText
; you can give a
prompt line, and you can press Enter after entering text.
The %s
code requires some explanation. Although you might at
first think it is a C-style formating code, as you would use in
string.format
, it actually describes how the value is going to edited;
%s
here merely means that a regular text box is used; if you had used
%m
, then a multiline edit box (like that used by iup.GetText
)
would be used.
If there is a limited set of choices, then the %l
format is
useful:
res, prof = iup.GetParam("Title", nil,
"Give your profession: %l|Teacher|Explorer|Engineer|\n",0)
Note the |item1|item2|...|
list after the %l
format; these are the choices presented to the user. The initial value you give
it, and the value you receive from it, are going to be an index into this list
of choices. Somewhat confusingly, they start at 0 (which is not the Lua way!) So
in this case, 0
means that 'Teacher' is to be selected, and if I
then selected 'Engineer', the resulting value of prof
would be 2.
The %i
code allows you to enter an integer value, with up/down
arrows for incrementing/decrementing the value.
require( "iuplua" )
require( "iupluacontrols" )
res, age = iup.GetParam("Title", nil,
"Give your age: %i\n",0)
if res ~= 0 then -- the user cooperated!
iup.Message("Really?",age)
end
GetParam
is a very versatile facility for asking for data, but
it is not very interactive. In general, you want to present something back to
the user that is more complicated than a simple message. Up to now we have used
the predefined dialogs available to IupLua; it is now time to go beyond that and
examine custom dialogs. The structure of a simple IupLua program is
straightforward:
require 'iuplua'
text = iup.multiline{expand = "YES"}
dlg = iup.dialog{text; title="Simple Dialog",size="QUARTERxQUARTER"}
dlg:show()
iup.MainLoop()
A multiline edit control is created, and put inside a window frame
with a given size, which is then made visible with the show
method.
We then enter the main loop of the application with MainLoop
, which
will only finish when the window is closed.
Controls are also windows, but without the frame and decorations of a
top-level window; they are always meant to be inside some window frame or
other container. We set the expand
attribute of
multiline
to force it to use up all the available space in the frame, so
that it takes its size from its container. The dialog's size
attribute is a string of the form "XSIZExYSIZE", where sizes can be expressed as
fractions of the desktop window size, in this case a quarter of the width and
height. (You can of course also use numerical sizes like "100x301" but these
will not always scale well on displays with different resolutions. See
Attributes/Common/SIZE in the manual for these units.)
It's good to pause a moment to look at the resulting application in action;
it is fully responsive and you can enter text, paste, etc. into the edit
control. Common keyboard shortcuts like Ctrl+V
and Ctrl+C
work as expected. All this functionality comes with the windowing system you are
currently using. On my system, Task Manager shows that this program uses 3.8 Meg
of memory, and an instance of Notepad uses 3.3 Meg, which represents all the
common code necessary to support a simple GUI application; you are not actually
paying much for using IupLua at all. The equivalent C program using the Windows
API would be about 150 lines, so the gain in programmer efficiency is
tremendous!
Of course, there is not much interaction possible with such a simple program.
To make a program respond to the user we define callbacks which the
system calls when some event takes place. For example, we can put a button
control in the dialog, and define its action
callback:
require 'iuplua'
btn = iup.button{title = "Click me!"}
function btn:action ()
iup.Message("Note","I have been clicked!")
return iup.DEFAULT
end
dlg = iup.dialog{btn; title="Simple Dialog",size="QUARTERxQUARTER"}
dlg:show()
iup.MainLoop()
This is perfectly responsive, although not very useful! The button sizes
itself to its natural size since expand
is not set (try setting
expand
to see the button fill the whole window frame.) Callbacks usually
return the special value iup.DEFAULT
, although in IupLua this is
not really necessary.
dialog
takes only one control, so IupLua defines containers in
which you can pack as many controls as you like. Here vbox
is used
to pack two buttons into the dialog vertically (To save space I'm leaving out
the 'dlg:show...' common code at the bottom)
btn1 = iup.button{title = "Click me!"}
btn2 = iup.button{title = "and me!"}
function btn1:action ()
iup.Message("Note","I have been clicked!")
end
function btn2:action ()
iup.Message("Note","Me too!")
end
box = iup.vbox {btn1,btn2}
dlg = iup.dialog{box; title="Simple Dialog",size="QUARTERxQUARTER"}
This does the job, although the buttons are sized differently according to
their contents; this program would not win any design contests! Still, you now
have two commands in your application. You can actually get a more pleasing
result by using a horizontal packing box (hbox
) and specifying a
non-zero gap between the buttons:
box = iup.hbox {btn1,btn2; gap=4}
You can nest boxes as much as you like, which is the way to construct more complicated layouts. Here are our horizontal buttons packed vertically with a multiline edit control:
bbox = iup.hbox {btn1,btn2; gap=4} text = iup.multiline{expand = "YES"} vbox = iup.vbox{bbox,text}
dlg = iup.dialog{vbox; title="Simple Dialog",size="QUARTERxQUARTER"}
We have effectively implemented a crude but functional toolbar:
A labeled frame can be put around a control using iup.frame
:
edit1 = iup.multiline{expand="YES",value="Number 1"}
edit2 = iup.multiline{expand="YES",value="Number 2?"}
box = iup.hbox {iup.frame{edit1;Title="First"},iup.frame{edit2;Title="Second"}}
A useful way to present various views to a user is to put them in tabs. This
places each control in a separate page, accessible through the tabbar at the
top. Notice in this example that the titles of the tab pages are actually set as
attributes of the pages through tabtitle
. This is not one
of the standard IUP controls (see Controls/Additional in the manual) so we also
need to bring in the iupluacontrols
library.
require( "iupluacontrols" )
edit1 = iup.multiline{expand="YES",value="Number 1",tabtitle="First"}
edit2 = iup.multiline{expand="YES",value="Number 2?",tabtitle="Second"}
tabs = iup.tabs{edit1,edit2,expand='YES'}
Sometimes a program needs to wake up and perform some operation, such as a
scheduled backup or an autosave operation. IupLua provides timers for
this purpose. (Note at this point that there is no reason why you can't have
print
in a IupLua application; sometimes there is no better way to track
what's going on. But on Windows you do have to run the program using the regular
lua.exe, not wlua.exe.)
-- timer1.lua
require "iuplua"
timer = iup.timer{time=500}
btn = iup.button {title = "Stop",expand="YES"}
function btn:action ()
if btn.title == "Stop" then
timer.run = "NO"
btn.title = "Start"
else
timer.run = "YES"
btn.title = "Stop"
end
end
function timer:action_cb()
print 'timer!'
end
timer.run = "YES"
dlg = iup.dialog{btn; title="Timer!"}
dlg:show()
iup.MainLoop()
After a timer has been started by setting its run
attribute to
"YES", it will continue to call action_cb
using the given time in
milliseconds. Notice that it is important to set the timer going only after the
callback has been defined. It is perfectly permissable to switch a timer off in
the callback, which is how you can perform a single action after waiting for
some time.
It is a well-known fact that computers spend most of the time doing very little, waiting for incredibly slow humans to type something new. However, when a computer is actually doing intense processing, users become impatient if not told about progress. If you do your lengthy processing directly, then the windows of the application become unresponsive. The proper way to organize such work is to do it when the system is idle.
IupLua provides a gauge control which is intended to show progress; this little program shows that even when the computer is almost completely preoccupied doing work, it is still keeping the user informed and in fact the window remains useable, although a little slow to respond.
-- idle1.lua
require "iuplua"
require "iupluacontrols"
function do_something ()
for i=1,6e7 do end
end
gauge = iup.gauge{show_text="YES"}
function idle_cb()
local value = gauge.value
do_something()
value = value + 0.1
if value > 1.0 then
value = 0.0
end
gauge.value = value
return iup.DEFAULT
end
dlg = iup.dialog{gauge; title = "IupGauge"}
iup.SetIdle(idle_cb)
dlg:showxy(iup.CENTER, iup.CENTER)
iup.MainLoop()
It is easy to display a list of values in IupLua. The values can be directly
specified in the iup.list
constructor, like so:
-- list1.lua
require "iuplua"
list = iup.list {"Horses","Dogs","Pigs","Humans"; expand="YES"}
dlg = iup.dialog{list; title="Lists"}
dlg:show()
iup.MainLoop()
(Remember that the single argument to these constructors is just a Lua table, which you can construct in any way you choose, say by reading the values from a file.)
Tracking selection changes is straightforward:
function list:action(t,i,v)
print(t,i,v)
end
Now, as I move the selection through the list, from the start to the finish, the output is:
Horses 1 1
Horses 1 0
Dogs 2 1
Dogs 2 0
Pigs 3 1
Pigs 3 0
Humans 4 1
So v
is 1 if we are selecting an item, 0 if we are deselecting
it; i
is the one-based index in the list, and t
is the
actual string value. If you want to associate some other data with each value,
then all you need to do is keep a table of that data and look it up using the
index i
.
To register a double-click is a little more involved. There is (as far as I
can tell) no way to detect whether a double-click has happened in the
action
callback. So we track the selection manually; if two selection
events for a given item happen consecutively, then that is understood to be a
double-click. It ain't pretty, but it works (except perhaps for the valid case
of a person wanting to double-click the same item repeatedly):
local lastidx,doubleclick
function on_double_click (t,i)
print(t,i)
end
function list:action(t, i, v)
if v ~= 0 then
if lastidx == i and doubleclick ~= i then
on_double_click(t,i)
doubleclick = i
end
lastidx = i
end
end
Once a list has been created, how does one change the contents? The answer is that the list object behaves like an array. For example, to fill a list with all the entries in a directory, I can use this function:
function fill (path)
local i = 1
for f in lfs.dir(path) do
list[i] = f
i = i + 1
end
list[i] = nil
end
Note that this does not mean that a list object is a table. In particular, you have to explicitly set the end of the list of elements by setting a nil value just after the end.
The most flexible way to present a hierarchy of information is a tree. A tree has a single root, and several branches. Each of these branches may have leaves, and other branches. All of these are called nodes. Thinking of a family tree, a node may have child nodes, which all share the same parent node.
A good example of this in everyday computer experience is a filesystem, where
the leaves are files and the branches are directories. Lua tables naturally
express these kind of nested structures easily, and in fact it is easy
to present a Lua table as a tree, where array items are leaves, and the branches
are named with the special field branchname
:
require 'iuplua'
require 'iupluacontrols'
tree = iup.tree{}
tree.addexpanded = "NO"
list = {
{
"Horse",
"Whale";
branchname = "Mammals"
},
{
"Shrimp",
"Lobster";
branchname = "Crustaceans"
},
{
branchname = "Birds"
},
branchname = "Animals"
}
iup.TreeAddNodes(tree, list)
f = iup.dialog{tree; title = "Tree Test"}
f:show()
iup.MainLoop()
This example begins with the branches 'collapsed', and you will have to
explicitly expand them with a mouse click. By default, trees are presented in
their fully expanded form; try taking out the fourth line that sets the
addexpanded
attribute of the tree object. Note that branches can be
empty!
Tree operations are naturally more complicated than list operations, but there is a callback which happens when a node is selected or unselected. Add this to the example:
function tree:selection_cb (id,iselect)
print(id,iselect,tree.name)
end
You will see that iselect
is 0 for the unselection operation,
and 1 for selection; id is a tree node index. These indices are always in order
of appearance in a tree, starting at 0 for the root node. The name
attribute of the tree object is the text of the currently selected node.
A pair of useful callbacks are branchopencb
and
branchclosecb
. If you were displaying a potentially very large tree
(like your computer's filesystem) then it would be inefficient to create the
whole tree at once, especially considering that you would normally be only
interested in a small part of that tree. Trapping branchopencb
allows you to add child nodes to your selected node before it is expanded.
executeleafcb
is called when you double-click on a leaf, as if you
were running a program in a file explorer.
In itself, the id is not particularly useful. The id order is always the same
in the tree, so as nodes get added and removed, the id of a particular node will
change. Generally, there is going to be some deeper data associated with a node.
On a filesystem, a node represents a full path to a file or directory, or there
may be an ip address associated with a computer name. IUP provides you with a
way to associate arbitrary data with nodes even if the id changes. But to use
this you will have to understand how to build up a tree from scratch -
TreeAddNodes
is very convenient, but won't help you if you have to add
nodes later. Replace the definition of list
and the call to
TreeAddNodes
with this code:
tree.name = "Animals"
tree.addbranch = "Birds"
tree.addbranch = "Crustaceans"
tree.addbranch = "Mammals"
You will get the top level branches of the tree; notice that they are
specified in reverse order, since nodes are always added to the top. Also note
the curious way in which the addbranch
attribute is used. For a
start, it is write-only, and the effect of setting a value to it is to
add a new branch to the currently selected node. By default, this starts out as
the root (which is set using the name
attribute) The id of the root
is always 0; when we add "Birds", the new branch has id 1, again when we add
"Crustaceans" the new branch also has id 1 - by which time "Birds" has moved to
id 2, further down the tree.
To add leaves, a similar process:
tree.name = "Animals"
tree.addbranch = "Mammals"
tree.addleaf1 = "Whale"
The addleaf
attribute works like addbranch
, and
both of them can take an extra parameter, which is the id of the node to add to.
In this case, "Whale" is a child leaf of the "Mammals" branch, which has id 1
at this stage. This new leaf gets an id of 2, which is one more than the
parent. So this gives us a way to build up arbitrary trees, knowing the id at
each point. IUP provides a function TreeSetUserId which can
associate a Lua table with a node id. We can choose to put a string value inside
this table, but it really can contain anything. Here is the first example, using
some helper functions to simplify matters:
-- testtree2.lua
require 'iuplua'
require 'iupluacontrols'
tree = iup.tree{}
function assoc (idx,value)
iup.TreeSetUserId(tree,idx,{value})
end
function addbranch(self,label,value)
self.addbranch = label
assoc(1,value or label)
end
function addleaf(self,label,value)
self.addleaf1 = label
assoc(2,value or label)
end
tree.name = "Animals"
addbranch(tree,"Birds")
addbranch(tree,"Crustaceans")
addleaf(tree,"Shrimp")
addleaf(tree,"Lobster")
addbranch(tree,"Mammals")
addleaf(tree,"Horse")
addleaf(tree,"Whale")
function dump (tp,id)
local t = iup.TreeGetUserId(tree,id)
-- our string data is always the first element of this table
print(tp,id,t and t[1])
end
function tree:branchopen_cb(id)
dump('open',id)
end
function tree:selection_cb (id,iselect)
if iselect == 1 then dump('select',id) end
end
f = iup.dialog{tree; title = "Tree Test"}
f:show()
iup.MainLoop()
There is a corresponding function TreeGetUserId which accesses
the table associated with the node id. There is also a function
TreeGetId which will return the id, given the unique table
associated with it. You can use this to programmatically select a tree node
given its data by setting the value
attribute to the returned id.
Now let's do something interesting with a tree control, a simple file browser. It is straightforward to get the files and directories contained within a directory:
require 'lfs'
local append = table.insert
function get_dir (path)
local files = {}
local dirs = {}
for f in lfs.dir(path) do
if f ~= '.' and f ~= '..' then
if lfs.attributes(path..'/'..f,'mode') == 'file' then
append(files,f)
else
append(dirs,f)
end
end
end
return files,dirs
end
We ignore '.' and '..' (the current and parent directory respectively) and
check the mode to see if we have file or a directory; this requires the full
path to be passed to attributes
. This function returns two separate
tables containing the names of the files and directories.
It is useful to define two helper functions for setting and getting data to be associated with the tree nodes:
tree = iup.tree {}
function set (id,value,attrib)
iup.TreeSetUserId(tree,id,{value,attrib})
end
function get(id)
return iup.TreeGetUserId(tree,id)
end
Filling a tree with the contents of a directory is straightforward. We want
the directories before the files, so we put them in last; nodes must be added in
reverse order! The id of the new nodes will always be id+1
where
id
is going to be the directory which we are filling. The fullpath plus a
field indicating whether we are a directory is associated with each new item:
function fill (path,id)
local files,dirs = get_dir(path)
id = id + 1
local state = "STATE"..id
for i = #files,1,-1 do -- put the files in reverse order!
tree.addleaf = files[i]
set(id,path..'/'..files[i])
end
for i = #dirs,1,-1 do -- ditto for directories!
tree.addbranch = dirs[i]
set(id,path..'/'..dirs[i],'dir')
tree[state] = "COLLAPSED"
end
end
By default, the directory branches will be created in their expanded form, so
we use the STATE
attribute to force them into their collapsed
state. Normally you would say this in Lua like so state2 = "COLLAPSED"
but here we build up the appropriate attribute string with the given id and use
array indexing to set the tree attribute.
Just calling fill('.',0)
and putting the tree into a dialog as
usual will give you a directory listing of the current directory! But it would
be cool if expanding a directory node would automatically fill that node; it
would obviously be wasteful to fill the whole tree at startup, since your
filesystem contains thousands of files. The branchopencb
callback is called when a user tries to expand a directory. We use this to fill
the directory with its contents, but only on the _first time that we expand
this node:
function tree:branchopen_cb(id)
tree.value = id
local t = get(id)
if t[2] == 'dir' then
fill(t[1],id)
set(id,t[1],'xdir')
end
end
This is why directories need to be specially marked, so we can tell later whether we have actually generated the contents of that directory!
The first statement of this function makes the node we are opening to be the selected node of the tree. (Although we are passed the correct id of the node, it seems to be necessary to perform this step to make things work correctly.)
See directory.lua
in the examples folder.
Any application that can perform a number of operations needs a menu. These
are not difficult to create in Iuplua, although it can be a little tedious to
set up. The basic idea is this: create some items, make a menu out of
these items, and set the menu
attribute of the dialog. The items
have an associated action
callback, which actually performs the
operation.
-- simple-menu.lua
require( "iuplua" )
-- Creates a text, sets its value and turns on text readonly mode
text = iup.text {readonly = "YES", value = "Show or hide this text"}
item_show = iup.item {title = "Show"}
item_hide = iup.item {title = "Hide"}
item_exit = iup.item {title = "Exit"}
function item_show:action()
text.visible = "YES"
return iup.DEFAULT
end
function item_hide:action()
text.visible = "NO"
return iup.DEFAULT
end
function item_exit:action()
return iup.CLOSE
end
menu = iup.menu {item_show,item_hide,item_exit}
-- Creates dialog with a text, sets its title and associates a menu to it
dlg = iup.dialog{text; title="Menu Example", menu=menu}
-- Shows dialog in the center of the screen
dlg:showxy(iup.CENTER,iup.CENTER)
iup.MainLoop()
A menu may contain items and submenus. This example shows a small function which makes creating arbitrarily complicated menus easier:
-- menu.lua
require( "iuplua" )
function default ()
iup.Message ("Warning", "Only Exit performs an operation")
return iup.DEFAULT
end
function do_close ()
return iup.CLOSE
end
mmenu = {
"File",{
"New",default,
"Open",default,
"Close",default,
"-",nil,
"Exit",do_close,
},
"Edit",{
"Copy",default,
"Paste",default,
"-",nil,
"Format",{
"DOS",default,
"UNIX",default
}
}
}
function create_menu(templ)
local items = {}
for i = 1,#templ,2 do
local label = templ[i]
local data = templ[i+1]
if type(data) == 'function' then
item = iup.item{title = label}
item.action = data
elseif type(data) == 'nil' then
item = iup.separator{}
else
item = iup.submenu {create_menu(data); title = label}
end
table.insert(items,item)
end
return iup.menu(items)
end
-- Creates a text, sets its value and turns on text readonly mode
text = iup.text {value = "Some text", expand = "YES"}
-- Creates dialog with a text, sets its title and associates a menu to it
dlg = iup.dialog {text; title = "Creating Menus With a Table",
menu = create_menu(mmenu), size = "QUARTERxEIGHTH"}
-- Shows dialog in the center of the screen
dlg:showxy (iup.CENTER,iup.CENTER)
iup.MainLoop()
The function create_menu
does all the work; we provide it with a
Lua table containing pairs of values; the first value of a pair is always a
string, and will be the label. The second value can either be a function, in
which case it represents an item to be associated with a callback, or nil
,
which means that it's a separator, or otherwise must be a table, which
represents a submenu. It is a nice example of how recursion can naturally handle
nested structures like menus, and how Lua's flexible table definitions can make
specifying such structures easy. This useful function is available in the
iupx
utility library as iupx.menu
.
Many kinds of numerical data are best seen as X-Y plots. iup.plot
is a control which can show several kinds of plots; you can have lines between
points, show them as markers, or both together. Several series (or datasets)
can be shown on a single plot, and a simple legend can be shown. The plot will
automatically scale to view all datasets, but the default minimum and maximum x
and y values can be changed. It is even possible to select points and edit them
on the plot.
A simple plot is straightforward:
-- plot1.lua
require( "iuplua" )
require( "iupluacontrols" )
require( "iuplua_plot51" )
plot = iup.plot{TITLE = "A simple XY Plot",
MARGINBOTTOM="35",
MARGINLEFT="35",
AXS_XLABEL="X",
AXS_YLABEL="Y"
}
iup.PlotBegin(plot,0)
iup.PlotAdd(plot,0,0)
iup.PlotAdd(plot,5,5)
iup.PlotAdd(plot,10,7)
iup.PlotEnd(plot)
dlg = iup.dialog{plot; title="Plot Example",size="QUARTERxQUARTER"}
dlg:show()
iup.MainLoop()
Creating a dataset involves calling PlotBegin
, a number of
calls to PlotAdd
to add data points, and finally a call to
PlotEnd
. You can create multiple datasets (or series) using multiple
begin/end calls, and can of course use loops to add points:
iup.PlotBegin(plot,0)
for x = -2,2,0.01 do
iup.PlotAdd(plot,x,math.sin(x))
end
iup.PlotEnd(plot)
iup.PlotBegin(plot,0)
for x = -2,2,0.01 do
iup.PlotAdd(plot,x,math.cos(x))
end
iup.PlotEnd(plot)
plot.DS_LINEWIDTH = 3
A limitation of the plot
library is that it does not choose
appropriate sizes for the plot margins. So I've had to set the bottom and left
margins (in pixels) to properly accomodate the axes and their titles. As with
all IupLua attributes, you can choose to make them upper case if you like; a full
list is found in the manual under Controls/Additional/IupPlot. Some of these
attributes refer to the plot as a whole, some to the current dataset.
For instance, setting GRID
to "YES" will draw gridlines for both
axes, but if we set DS_LINEWIDTH
to 3 after the construction of the
cosine dataset, then only that line is affected.
Some attributes affect others. DS_MODE
is used to specify how to
draw the dataset; it can be "LINE", "BAR", (for a bar chart) "MARK" (just for
marks) or "MARKLINE" (for lines and marks). But it has to be set before any of
the other DS_
attributes like DS_MARKSIZE
, etc. In
another case, you will often find it useful to set an explicit minimum y value
by setting AXS_YMIN
. But it will only take effect if
AXIS_YAUTOMIN
has been set to "NO" to disable auto scaling.
As with menus, making a Lua-friendly wrapper around an API is not difficult and can be very labour-saving. It would be clearer if we could work with the plot object in a more object-oriented way:
plot:Begin()
for i = 1,#xvalues do
plot:Add(xvalues[i],yvalues[i])
end
plot:End()
And for the common case where you have arrays of values, it would be convenient to be able to say:
plot:AddSeries({{0,1.5},{5,4.5},{10,7.6}},{DS_MODE="MARK"})
Here is a function which wraps the Plot
API:
function create_plot (tbl)
-- don't need to remember this anymore!
require( "iuplua_plot51" )
-- the defaults for these values are too small, at least on my system!
if not tbl.MARGINLEFT then tbl.MARGINLEFT = 30 end
if not tbl.MARGINBOTTOM then tbl.MARGINBOTTOM = 35 end
-- if we explicitly supply ranges, then auto must be switched off for that direction.
if tbl.AXS_YMIN then tbl.AXS_YAUTOMIN = "NO" end
if tbl.AXS_YMAX then tbl.AXS_YAUTOMAX = "NO" end
if tbl.AXS_XMIN then tbl.AXS_XAUTOMIN = "NO" end
if tbl.AXS_XMAX then tbl.AXS_XAUTOMAX = "NO" end
local plot = iup.plot(tbl)
plot.End = iup.PlotEnd
plot.Add = iup.PlotAdd
function plot.Begin ()
return iup.PlotBegin(plot,0)
end
function plot:AddSeries(xvalues,yvalues,options)
plot:Begin()
-- is xvalues a table of (x,y) pairs?
if type(xvalues[1]) == "table" then
-- because there's only one data table, the next must be options
options = yvalues
for i,v in ipairs(xvalues) do
plot:Add(v[1],v[2])
end
else
for i = 1,#xvalues do
plot:Add(xvalues[i],yvalues[i])
end
end
plot:End()
-- set any series-specific plot attributes
if options then
-- mode must be set before any other attributes!
if options.DS_MODE then
plot.DS_MODE = options.DS_MODE
options.DS_MODE = nil
end
for k,v in pairs(options) do
plot[k] = v
end
end
end
function plot:Redraw()
plot.REDRAW='YES'
end
return plot
end
This function creates a Plot
object as usual, but supplies some
more sensible defaults for the margins, makes setting things like AXS_XMAX
also set AXS_XAUTOMAX
, and adds some new methods to the object. Of
these, AddSeries
is the interesting one. It allows you to specify
the data in two forms; either as two arrays of x and y values, or as a single
array of x-y pairs. It also allows optionally setting DS_
attributes, taking care to set the plot mode before any other attributes. In
this way, the actual details can be hidden away from the programmer, who has
then less things to worry about.
Given this function, we can write a little program which plots some points and draws the linear least-squares fit between them:
-- simple-plot.lua
local xx = {0,2,5,10}
local yy = {1,1.5,6,8}
function least_squares (xx,yy)
local xsum = 0.0
local ysum = 0.0
local xxsum = 0.0
local yysum = 0.0
local xysum = 0.0
local n = #xx
for i = 1,n do
local x,y = xx[i], yy[i]
xsum = xsum + x
ysum = ysum + y
xxsum = xxsum + x*x
yysum = yysum + y*y
xysum = xysum + x*y
end
local m = (xsum*ysum/n - xysum )/(xsum*xsum/n - xxsum)
local c = (ysum - m*xsum)/n
return m,c
end
local m,c = least_squares(xx,yy)
function eval (x) return m*x + c end
local plot = create_plot {TITLE = "Simple Data",AXS_YMIN=0,GRID="YES"}
-- the original data
plot:AddSeries(xx,yy,{DS_MODE="MARK",DS_MARKSTYLE="CIRCLE"})
-- the least squares fit
local xmin,xmax = xx[1],xx[#xx]
plot:AddSeries({xmin,xmax},{eval(xmin),eval(xmax)})
create_plot
is so useful that I've packaged it as part of the
iupx
library as iupx.plot
. A new pseudo-attribute has been
introduced, AXS_BOUNDS
, which is a table of four values
{xmin,ymin,xmax,ymax}
. This example shows that very different ranges can
happily exist on the same plot:
-- plot5.lua
require "iupx"
plot = iupx.plot {TITLE = "Simple Data", AXS_BOUNDS={0,0,100,100}}
plot:AddSeries ({{0,0},{10,10},{20,30},{30,45}})
plot:AddSeries ({{40,40},{50,55},{60,60},{70,65}})
iupx.show_dialog{plot; title="Easy Plotting",size="QUARTERxQUARTER"}