Drag and drop files in Matlab GUI - matlab

I am trying to find a way to use drag and drop in my matlab GUI. The closest I've found is this.
However, I would like the result to look like this:
When a file has been dropped, all I need is the path of the file and a call to my load function.
All suggestions are much appreciated!

This submission by Maarten van der Seijs on the file exchange seems to solve it.
It creates a callback function which can be coupled to java swing GUI components, as shown in the attached demo.
It uses a java class, which is a thin wrapper around java.awt.dnd.DropTarget:
import java.awt.dnd.*;
import java.awt.datatransfer.*;
import java.util.*;
import java.io.File;
import java.io.IOException;
public class MLDropTarget extends DropTarget
{
/**
* Modified DropTarget to be used for drag & drop in MATLAB UI control.
*/
private static final long serialVersionUID = 1L;
private int droptype;
private Transferable t;
private String[] transferData;
public static final int DROPERROR = 0;
public static final int DROPTEXTTYPE = 1;
public static final int DROPFILETYPE = 2;
#SuppressWarnings("unchecked")
#Override
public synchronized void drop(DropTargetDropEvent evt) {
// Make sure drop is accepted
evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
// Set droptype to zero
droptype = DROPERROR;
// Get transferable and analyze
t = evt.getTransferable();
try {
if (t.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
// Interpret as list of files
List<File> fileList = (ArrayList<File>) t.getTransferData(DataFlavor.javaFileListFlavor);
transferData = new String[fileList.size()];
for (int i = 0; i < fileList.size(); i++)
transferData[i] = fileList.get(i).getAbsolutePath();
droptype = DROPFILETYPE;
}
else if (t.isDataFlavorSupported(DataFlavor.stringFlavor)) {
// Interpret as string
transferData[0] = (String) t.getTransferData(DataFlavor.stringFlavor);
droptype = DROPTEXTTYPE;
}
} catch (UnsupportedFlavorException e) {
droptype = DROPERROR;
super.drop(evt);
return;
} catch (IOException e) {
droptype = DROPERROR;
super.drop(evt);
return;
}
// Call built-in drop method (fire MATLAB Callback)
super.drop(evt);
}
public int getDropType() {
return droptype;
}
public Transferable getTransferable() {
return t;
}
public String[] getTransferData() {
return transferData;
}
}
which is then initialized and called by a MATLAB class:
classdef (CaseInsensitiveProperties) dndcontrol < handle
%DNDCONTROL Class for Drag & Drop functionality.
% obj = DNDCONTROL(javaobj) creates a dndcontrol object for the specified
% Java object, such as 'javax.swing.JTextArea' or 'javax.swing.JList'. Two
% callback functions are available: obj.DropFileFcn and obj.DropStringFcn,
% that listen to drop actions of respectively system files or plain text.
%
% The Drag & Drop control class relies on a Java class that need to be
% visible on the Java classpath. To initialize, call the static method
% dndcontrol.initJava(). The Java class can be adjusted and recompiled if
% desired.
%
% DNDCONTROL Properties:
% Parent - The associated Java object.
% DropFileFcn - Callback function for system files.
% DropStringFcn - Callback function for plain text.
%
% DNDCONTROL Methods:
% dndcontrol - Constructs the DNDCONTROL object.
%
% DNDCONTROL Static Methods:
% defaultDropFcn - Default callback function for drop events.
% demo - Runs the demonstration script.
% initJava - Initializes the Java class.
% isInitialized - Checks if the Java class is visible.
%
% A demonstration is available from the static method dndcontrol.demo().
%
% Example:
% dndcontrol.initJava();
% dndcontrol.demo();
%
% See also:
% uicontrol, javaObjectEDT.
%
% Written by: Maarten van der Seijs, 2015.
% Version: 1.0, 13 October 2015.
properties (Hidden)
dropTarget;
end
properties (Dependent)
%PARENT The associated Java object.
Parent;
end
properties
%DROPFILEFCN Callback function executed upon dropping of system files.
DropFileFcn;
%DROPSTRINGFCN Callback function executed upon dropping of plain text.
DropStringFcn;
end
methods (Static)
function initJava()
%INITJAVA Initializes the required Java class.
%Add java folder to javaclasspath if necessary
if ~dndcontrol.isInitialized();
classpath = fileparts(mfilename('fullpath'));
javaclasspath(classpath);
end
end
function TF = isInitialized()
%ISINITIALIZED Returns true if the Java class is initialized.
TF = (exist('MLDropTarget','class') == 8);
end
end
methods
function obj = dndcontrol(Parent,DropFileFcn,DropStringFcn)
%DNDCONTROL Drag & Drop control constructor.
% obj = DNDCONTROL(javaobj) contstructs a DNDCONTROL object for
% the given parent control javaobj. The parent control should be a
% subclass of java.awt.Component, such as most Java Swing widgets.
%
% obj = DNDCONTROL(javaobj,DropFileFcn,DropStringFcn) sets the
% callback functions for dropping of files and text.
% Check for Java class
assert(dndcontrol.isInitialized(),'Javaclass MLDropTarget not found. Call dndcontrol.initJava() for initialization.')
% Construct DropTarget
obj.dropTarget = handle(javaObjectEDT('MLDropTarget'),'CallbackProperties');
set(obj.dropTarget,'DropCallback',{#dndcontrol.DndCallback,obj});
set(obj.dropTarget,'DragEnterCallback',{#dndcontrol.DndCallback,obj});
% Set DropTarget to Parent
if nargin >=1, Parent.setDropTarget(obj.dropTarget); end
% Set callback functions
if nargin >=2, obj.DropFileFcn = DropFileFcn; end
if nargin >=3, obj.DropStringFcn = DropStringFcn; end
end
function set.Parent(obj, Parent)
if isempty(Parent)
obj.dropTarget.setComponent([]);
return
end
if isa(Parent,'handle') && ismethod(Parent,'java')
Parent = Parent.java;
end
assert(isa(Parent,'java.awt.Component'),'Parent is not a subclass of java.awt.Component.')
assert(ismethod(Parent,'setDropTarget'),'DropTarget cannot be set on this object.')
obj.dropTarget.setComponent(Parent);
end
function Parent = get.Parent(obj)
Parent = obj.dropTarget.getComponent();
end
end
methods (Static, Hidden = true)
%% Callback functions
function DndCallback(jSource,jEvent,obj)
if jEvent.isa('java.awt.dnd.DropTargetDropEvent')
% Drop event
try
switch jSource.getDropType()
case 0
% No success.
case 1
% String dropped.
string = char(jSource.getTransferData());
if ~isempty(obj.DropStringFcn)
evt = struct();
evt.DropType = 'string';
evt.Data = string;
feval(obj.DropStringFcn,obj,evt);
end
case 2
% File dropped.
files = cell(jSource.getTransferData());
if ~isempty(obj.DropFileFcn)
evt = struct();
evt.DropType = 'file';
evt.Data = files;
feval(obj.DropFileFcn,obj,evt);
end
end
% Set dropComplete
jEvent.dropComplete(true);
catch ME
% Set dropComplete
jEvent.dropComplete(true);
rethrow(ME)
end
elseif jEvent.isa('java.awt.dnd.DropTargetDragEvent')
% Drag event
action = java.awt.dnd.DnDConstants.ACTION_COPY;
jEvent.acceptDrag(action);
end
end
end
methods (Static)
function defaultDropFcn(src,evt)
%DEFAULTDROPFCN Default drop callback.
% DEFAULTDROPFCN(src,evt) accepts the following arguments:
% src - The dndcontrol object.
% evt - A structure with fields 'DropType' and 'Data'.
fprintf('Drop event from %s component:\n',char(src.Parent.class()));
switch evt.DropType
case 'file'
fprintf('Dropped files:\n');
for n = 1:numel(evt.Data)
fprintf('%d %s\n',n,evt.Data{n});
end
case 'string'
fprintf('Dropped text:\n%s\n',evt.Data);
end
end
function [dndobj,hFig] = demo()
%DEMO Demonstration of the dndcontrol class functionality.
% dndcontrol.demo() runs the demonstration. Make sure that the
% Java class is visible in the Java classpath.
% Initialize Java class
dndcontrol.initJava();
% Create figure
hFig = figure();
% Create Java Swing JTextArea
jTextArea = javaObjectEDT('javax.swing.JTextArea', ...
sprintf('Drop some files or text content here.\n\n'));
% Create Java Swing JScrollPane
jScrollPane = javaObjectEDT('javax.swing.JScrollPane', jTextArea);
jScrollPane.setVerticalScrollBarPolicy(jScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
% Add Scrollpane to figure
[~,hContainer] = javacomponent(jScrollPane,[],hFig);
set(hContainer,'Units','normalized','Position',[0 0 1 1]);
% Create dndcontrol for the JTextArea object
dndobj = dndcontrol(jTextArea);
% Set Drop callback functions
dndobj.DropFileFcn = #demoDropFcn;
dndobj.DropStringFcn = #demoDropFcn;
% Callback function
function demoDropFcn(~,evt)
switch evt.DropType
case 'file'
jTextArea.append(sprintf('Dropped files:\n'));
for n = 1:numel(evt.Data)
jTextArea.append(sprintf('%d %s\n',n,evt.Data{n}));
end
case 'string'
jTextArea.append(sprintf('Dropped text:\n%s\n',evt.Data));
end
jTextArea.append(sprintf('\n'));
end
end
end
end

Related

How to load seperate .mat files using a drop down component in App Designer?

I have separate .mat files, trials in a study, which consits of the same variables but the value change between files.
I want to use a drop-down component in MATLAB app designer to select a file, load its variables and display various plots.
Any ideas how I could do this?
Thank you in advance.
I had been loading a single file as a property like this:
var = load('Trial1.mat')
This code worked.
So then I tried to use this layout to load the files using the drop down menu but it didn't work..
function SelectFileDropDownValueChanged(app, event)
value = app.SelectFileDropDown.Value;
if strcmp(value,'Trial 1')
var = load('Trial1.mat');
elseif strcmp(value,'Trial 2')
var = load('Trial2.mat');
elseif strcmp(value,'Trial 3')
var = load('Trial3.mat');
elseif strcmp(value,'Trial 4')
var = load('Trial4.mat');
elseif strcmp(value,'Trial5')
var = load('Trial5.mat');
end
Any ideas how I could do this?
Thanks in advance!
You can do it as follows:
Add a new property named var to the App class.
You can add a private (or public) property in the EDITOR tab of the Designer:
Change the property name to var (matching your code sample).
Properties code block:
properties (Access = private)
var % Description: store loaded variables
end
Now var is a class member.
The App Designer is based on Object-Oriented MATLAB Programming.
Accessing var is possible only withing the code of the App class (because it's a private member).
Accessing var property is as follows: app.var (app is a reference to class' object).
Note that app is the first parameter of class methods (as in SelectFileDropDownValueChanged method).
Modified SelectFileDropDownValueChanged code:
% Value changed function: SelectFileDropDown
function SelectFileDropDownValueChanged(app, event)
value = app.SelectFileDropDown.Value;
if strcmp(value,'Trial 1')
app.var = load('Trial1.mat');
elseif strcmp(value,'Trial 2')
app.var = load('Trial2.mat');
elseif strcmp(value,'Trial 3')
app.var = load('Trial3.mat');
elseif strcmp(value,'Trial 4')
app.var = load('Trial4.mat');
elseif strcmp(value,'Trial5')
app.var = load('Trial5.mat');
end
end

Triggering and listening to an event

I have a class that represent some data and a (short) chain of results originating from this data. The class has the three properties Data1, Data2 and Data3, where Data2 depends on Data1 (it is actually a filtered version of it) and Data3 depends on Data2.
My idea was to trigger an event when Data1 changes that computes Data2, and then trigger an other event that leads to the computation of Data3. However, in the test class below, Data3 is empty after construction.
Note that I use uiw.event.EventData from the Widget toolbox.
classdef test < handle
events
DataChanged
end
properties
DataChangedListener event.listener
end
properties (AbortSet)
Data1 (:,:) double = []
Data2 (:,:) double = []
Data3 (:,:) double = []
end
methods
function self = test()
self.DataChangedListener = event.listener(self, 'DataChanged', #self.onDataChanged);
self.Data1 = peaks(64);
end
%% setter
function set.Data1(self, d)
self.Data1 = d;
evt = uiw.event.EventData( ...
'EventType', 'DataChanged', ...
'Property', 'Data1', ...
'Model', self );
self.notify('DataChanged', evt);
end
function set.Data2(self, d)
self.Data2 = d;
evt = uiw.event.EventData( ...
'EventType', 'DataChanged', ...
'Property', 'Data2', ...
'Model', self );
self.notify('DataChanged', evt);
end
function set.Data3(self, d)
self.Data3 = d;
evt = uiw.event.EventData( ...
'EventType', 'DataChanged', ...
'Property', 'Data3', ...
'Model', self );
self.notify('DataChanged', evt);
end
%% Event callback
function onDataChanged(self, ~, evt)
switch evt.Property
case 'Data1'
self.Data2 = self.Data1 + 5;
case 'Data2'
self.Data3 = self.Data2 + 10;
end
end
end
end
If one debugs this, one sees that onDataChanged is never called with the uiw.event.eventData with evt.Property = 'Data2'.
Das anyone know why and how to trigger the event properly?
I think I solved the puzzle.
Your code executes the listener callback recursively.
By default recursive listener is disabled.
Refer to Event and Listener Concepts:
Recursive — Allow listener to trigger the same event that caused execution of the callback.
Recursive is false by default. If the callback triggers the event for which it is defined as the callback, the listener cannot execute recursively. Therefore, set Recursive to false if the callback must trigger its own event. Setting the Recursive property to true can create a situation where infinite recursion reaches the recursion limit and triggers an error.
All you need to do is setting Recursive property to true:
After self.DataChangedListener = event.listener(self, 'DataChanged', #self.onDataChanged);
Add: self.DataChangedListener.Recursive = true;

how to access a gui object in a nested function from a base workspace

I think my issue is I don't know how to access guiel.hAX(3) property in my button callback function where I put kiddies = get(guiel.hAX(3),'Children');
I got the error:
Undefined variable "guiel" or class "guiel.hAX".
Error in showTF/callback_update_model (line 508)
kiddies = get(guiel.hAX(3),'Children');
My nested function for button callback:
function callback_update_model(~,~)
vars.dropheight = str2num(get(edit(2),'String'));
vars.armradius = str2num(get(edit(1),'String'));
kiddies = get(guiel.hAX(3),'Children');
delete(kiddies);
clear kiddies;
set(guiel.tfPanel,'Visible','off','Position',cnst.tfPanelpos);
set(guiel.hAX(1),'Position',cnst.axpos1);
if ishandle(guiel.hAX(2))
set(guiel.hAX(2),'Position',cnst.axpos2);
end
eval(get(guiel.hPB(4),'Callback'));
end
I initialize variables in other mfile
guiel.hAX(1) = -1;
guiel.hAX(2) = -1;
guiel.hAX(3) = -1;
guiel.tfPanel = -1;
...
guiel.hAX(3) = axes('Parent',guiel.tfPanel,'Color',cnst.OFFWHITE,'Layer',...
'top','Xlim',[0 1],'YLim',[0 1],'GridLineStyle','none','Units','Normalized',...
'XTick',[],'YTick',[],'Box','off','Visible','off','Position',cnst.axpos3);
To make data available in callbacks, you can store the data in the UserData property of the figure. If your figure handle is h_fig, then you would use code like this to store the data in the figure:
set(h_fig, 'UserData', guiel)
In your callback, you can use the gcbo function to get the handle to the figure, then extract the user data, use code like this:
[~, h_fig] = gcbo;
guiel = get(h_fig, 'UserData')

Matlab: How to survey compiled m-code progression from external API?

My question is extremely specific to the arcanes of the matlab compiler and runtime. As only people familiar with matlab runtime API may answer, I shortened much details. Please let me know if I should be more verbose.
Introduction
Using the matlab compiler & runtime I can call a function written in m-code from a C# program. Let's say calling:
function [result] = foo(n)
%[
result = 0;
for k = 1:n,
pause(1.0); % simulate long processing
result = result + 42;
end
%]
with (somewhere behind some dllimports in the C# code):
mclFeval(IntPtr inst, string name, IntPtr[] plhs, IntPtr[] prhs)
So far, so good, I have no issue with this (i.e intializing the runtime, loading the '.cft' file, marshalling back and forth MxArray with .Net types, etc...)
My Problem
I would like to survey the progression of my foo function using some cancel and progress callbacks:
function [result] = foo(n, cancelCB, progressCB)
%[
if (nargin < 3), progressCB = #(ratio, msg) disp(sprintf('Ratio = %f, Msg = %s', ratio, msg)); end
if (nargin < 2), cancelCB = #() disp('Checking cancel...'); end
result = 0;
for k = 1:n,
if (~isempty(cancelCB)),
cancelCB(); % Up to the callback to raise some error('cancel');
end;
if (~isempty(progressCB)),
progressCB(k/n, sprintf('Processing (%i/%i)', k, n));
end
pause(1.0); % simulate long processing
result = result + 42;
end
%]
But of course I would like these callbacks to be in the C# code, not within the m-one.
Investigations
Looking at 'mclmcr.h' header file, it looks like these functions may be of help:
extern mxArray* mclCreateSimpleFunctionHandle(mxFunctionPtr fcn);
extern bool mclRegisterExternalFunction(HMCRINSTANCE inst, const char* varname, mxFunctionPtr fcn);
Unfortunatly these are fully undocumented and I found no use case I could mimic to understand how they work.
I've also thought about creating a COM visible object in C# and pass it as a parameter to the matlab code:
// Somewhere within C# code:
var survey = new ComSurvey();
survey.SetCancelCallback = () => { if (/**/) throw new OperationCancelException(); };
survey.SetProgressCallback = (ratio, msg) => { /* do something */ };
function [result] = foo(n, survey)
%[
if (nargin < 2), survey = []; end
result = 0;
for k = 1:n,
if (~isempty(survey)),
survey.CheckCancel(); % up to the COM object to raise exception
survey.SetProgress(k/n, sprintf('Processing... %i/%i', k, n));
end
pause(1.0); % simulate long processing
result = result + 42;
end
%]
I'm very familiar with functions to create numeric and structure arrays and know how to use them:
extern mxArray *mxCreateNumericArray(...)
extern mxArray *mxCreateStructArray(...)
Anyhow, how COM objects are packaged to MxArrays, I don't know?
Further investigations
Day+1
Even if still unstable, I succeeded to have matlab to callback into my C# code and it seems that mclCreateSimpleFunctionHandle is the direction to go.
Note: Below code is for reference only. It may not be suitable in your own context as is. I'll provide simpler code later on (i.e. once I'll get stable solution).
Looking to the signature of the mxFunctionPtr, I created two delegates like this:
// Mimic low level signature for a Matlab function pointer
[UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
delegate void MCRInteropDelegate(int nlhs, IntPtr[] plhs, int nrhs, IntPtr[] prhs);
and
// Same signature (but far more elegant from .NET perspective)
delegate void MCRDelegate(MxArray[] varargouts, MxArray[] varargins);
I also linked to the runtime like this:
[DllImport("mclmcrrt74.dll", EntryPoint = "mclCreateSimpleFunctionHandle", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, ExactSpelling = true)]
static extern IntPtr _mclCreateSimpleFunctionHandle(MCRInteropDelegate fctn);
Assuming MxArray is a .NET class of mine that simply encapsulate for mxArray* handles, I then marshaled my delegates like this:
// Create MxArray from corresponding .NET delegate
static MxArray CreateFromDelegate(MCRDelegate del)
{
// Package high level delegate signature to a 'dllimport' signature
MCRInteropDelegate interopDel = (nlhs, plhs, nrhs, prhs) =>
{
int k = 0;
var varargouts = new MxArray[nlhs];
var varargins = new MxArray[nrhs];
// (nrhs, prhs) => MxArray[] varargins
Array.ForEach(varargins, x => new MxArray(prhs[k++], false)); // false = is to indicate that MxArray must not be disposed on .NET side
// Call delegate
del(varargouts, varargins); // Todo: varargouts created by the delegate must be destroyed by matlab, not by .NET !!
// MxArray[] varargouts => (nlhs, plhs)
k = 0;
Array.ForEach(plhs, x => varargouts[k++].getPointer());
};
// Create the 1x1 array of 'function pointer' type
return new MxArray(MCRInterop.mclCreateSimpleFunctionHandle(interopDel));
}
Finally, assuming module is an instance of MCRModule (again, a class of mine to encapsulate hInst* in low level mclFeval API), I was able to call foo function and have it to enter my .NET cancel delegate like this:
// Create cancel callback in .NET
MCRDelegate cancel = (varargouts, varargins) =>
{
if ((varargouts != null) && (varargouts.Length != 0) { throw new ArgumentException("'cancel' callback called with too many output arguments"); }
if ((varargins != null) && (varargins.Length != 0) { throw new ArgumentException("'cancel' callback called with too many input arguments"); }
if (...mustCancel...) { throw new OperationCanceledException(); }
}
// Enter the m-code
// NB: Below function automatically converts its parameters to MxArray
// and then call low level mclFeval with correct 'mxArray*' handles
module.Evaluate("foo", (double)10, cancel);
This .NET code worked fine, and foo really made callback to the cancel delegate properly.
Only problem, is that it is quite unstable. My guess is that I used too many anonymous functions, and probably some of them are disposed too early ...
Will try to provide with stable solution within the next few days (hopefully with simpler code to read and copy-paste in your own context for immediate testing).
Please let me know if you think I'm going the wrong direction with mclCreateSimpleFunctionHandle.
Got it
mclCreateSimpleFunctionHandle was effectively the right API function to call at in order to create an array variable (on matlab's side) holding for a function pointer (on external's side). I'm now able to have compiled m-code to call back into my C# code for cancellation and progression purposes.
Correct marshalling for mclCreateSimpleFunctionHandle is described here

How to create a class, subclass and properties in Lua?

I'm having a hard time grokking classes in Lua. Fruitless googling led me to ideas about meta-tables, and implied that third-party libraries are necessary to simulate/write classes.
Here's a sample (just because I've noticed I get better answers when I provide sample code):
public class ElectronicDevice
{
protected bool _isOn;
public bool IsOn { get { return _isOn; } set { _isOn = value; } }
public void Reboot(){_isOn = false; ResetHardware();_isOn = true; }
}
public class Router : ElectronicDevice
{
}
public class Modem :ElectronicDevice
{
public void WarDialNeighborhood(string areaCode)
{
ElectronicDevice cisco = new Router();
cisco.Reboot();
Reboot();
if (_isOn)
StartDialing(areaCode);
}
}
Here is my first attempt to translate the above using the technique suggested by Javier.
I took the advice of RBerteig. However, invocations on derived classes still yield: "attempt to call method 'methodName' (a nil value)"
--Everything is a table
ElectronicDevice = {};
--Magic happens
mt = {__index=ElectronicDevice};
--This must be a constructor
function ElectronicDeviceFactory ()
-- Seems that the metatable holds the fields
return setmetatable ({isOn=true}, mt)
end
-- Simulate properties with get/set functions
function ElectronicDevice:getIsOn() return self.isOn end
function ElectronicDevice:setIsOn(value) self.isOn = value end
function ElectronicDevice:Reboot() self.isOn = false;
self:ResetHardware(); self.isOn = true; end
function ElectronicDevice:ResetHardware() print('resetting hardware...') end
Router = {};
mt_for_router = {__index=Router}
--Router inherits from ElectronicDevice
Router = setmetatable({},{__index=ElectronicDevice});
--Constructor for subclass, not sure if metatable is supposed to be different
function RouterFactory ()
return setmetatable ({},mt_for_router)
end
Modem ={};
mt_for_modem = {__index=Modem}
--Modem inherits from ElectronicDevice
Modem = setmetatable({},{__index=ElectronicDevice});
--Constructor for subclass, not sure if metatable is supposed to be different
function ModemFactory ()
return setmetatable ({},mt_for_modem)
end
function Modem:WarDialNeighborhood(areaCode)
cisco = RouterFactory();
--polymorphism
cisco.Reboot(); --Call reboot on a router
self.Reboot(); --Call reboot on a modem
if (self.isOn) then self:StartDialing(areaCode) end;
end
function Modem:StartDialing(areaCode)
print('now dialing all numbers in ' .. areaCode);
end
testDevice = ElectronicDeviceFactory();
print("The device is on? " .. (testDevice:getIsOn() and "yes" or "no") );
testDevice:Reboot(); --Ok
testRouter = RouterFactory();
testRouter:ResetHardware(); -- nil value
testModem = ModemFactory();
testModem:StartDialing('123'); -- nil value
Here's an example literal transcription of your code, with a helpful Class library that could be moved to another file.
This is by no means a canonical implementation of Class; feel free to define your object model however you like.
Class = {}
function Class:new(super)
local class, metatable, properties = {}, {}, {}
class.metatable = metatable
class.properties = properties
function metatable:__index(key)
local prop = properties[key]
if prop then
return prop.get(self)
elseif class[key] ~= nil then
return class[key]
elseif super then
return super.metatable.__index(self, key)
else
return nil
end
end
function metatable:__newindex(key, value)
local prop = properties[key]
if prop then
return prop.set(self, value)
elseif super then
return super.metatable.__newindex(self, key, value)
else
rawset(self, key, value)
end
end
function class:new(...)
local obj = setmetatable({}, self.metatable)
if obj.__new then
obj:__new(...)
end
return obj
end
return class
end
ElectronicDevice = Class:new()
function ElectronicDevice:__new()
self.isOn = false
end
ElectronicDevice.properties.isOn = {}
function ElectronicDevice.properties.isOn:get()
return self._isOn
end
function ElectronicDevice.properties.isOn:set(value)
self._isOn = value
end
function ElectronicDevice:Reboot()
self._isOn = false
self:ResetHardware()
self._isOn = true
end
Router = Class:new(ElectronicDevice)
Modem = Class:new(ElectronicDevice)
function Modem:WarDialNeighborhood(areaCode)
local cisco = Router:new()
cisco:Reboot()
self:Reboot()
if self._isOn then
self:StartDialing(areaCode)
end
end
If you were to stick to get/set methods for properties, you wouldn't need __index and __newindex functions, and could just have an __index table. In that case, the easiest way to simulate inheritance is something like this:
BaseClass = {}
BaseClass.index = {}
BaseClass.metatable = {__index = BaseClass.index}
DerivedClass = {}
DerivedClass.index = setmetatable({}, {__index = BaseClass.index})
DerivedClass.metatable = {__index = DerivedClass.index}
In other words, the derived class's __index table "inherits" the base class's __index table. This works because Lua, when delegating to an __index table, effectively repeats the lookup on it, so the __index table's metamethods are invoked.
Also, be wary about calling obj.Method(...) vs obj:Method(...). obj:Method(...) is syntactic sugar for obj.Method(obj, ...), and mixing up the two calls can produce unusual errors.
There are a number of ways you can do it but this is how I do (updated with a shot at inheritance):
function newRGB(r, g, b)
local rgb={
red = r;
green = g;
blue = b;
setRed = function(self, r)
self.red = r;
end;
setGreen = function(self, g)
self.green= g;
end;
setBlue = function(self, b)
self.blue= b;
end;
show = function(self)
print("red=",self.red," blue=",self.blue," green=",self.green);
end;
}
return rgb;
end
purple = newRGB(128, 0, 128);
purple:show();
purple:setRed(180);
purple:show();
---// Does this count as inheritance?
function newNamedRGB(name, r, g, b)
local nrgb = newRGB(r, g, b);
nrgb.__index = nrgb; ---// who is self?
nrgb.setName = function(self, n)
self.name = n;
end;
nrgb.show = function(self)
print(name,": red=",self.red," blue=",self.blue," green=",self.green);
end;
return nrgb;
end
orange = newNamedRGB("orange", 180, 180, 0);
orange:show();
orange:setGreen(128);
orange:show();
I don't implement private, protected, etc. although it is possible.
If you don't want to reinvent the wheel, there is a nice Lua library implementing several object models. It's called LOOP.
The way I liked to do it was by implementing a clone() function.
Note that this is for Lua 5.0. I think 5.1 has more built-in object oriented constructions.
clone = function(object, ...)
local ret = {}
-- clone base class
if type(object)=="table" then
for k,v in pairs(object) do
if type(v) == "table" then
v = clone(v)
end
-- don't clone functions, just inherit them
if type(v) ~= "function" then
-- mix in other objects.
ret[k] = v
end
end
end
-- set metatable to object
setmetatable(ret, { __index = object })
-- mix in tables
for _,class in ipairs(arg) do
for k,v in pairs(class) do
if type(v) == "table" then
v = clone(v)
end
-- mix in v.
ret[k] = v
end
end
return ret
end
You then define a class as a table:
Thing = {
a = 1,
b = 2,
foo = function(self, x)
print("total = ", self.a + self.b + x)
end
}
To instantiate it or to derive from it, you use clone() and you can override things by passing them in another table (or tables) as mix-ins
myThing = clone(Thing, { a = 5, b = 10 })
To call, you use the syntax :
myThing:foo(100);
That will print:
total = 115
To derive a sub-class, you basically define another prototype object:
BigThing = clone(Thing, {
-- and override stuff.
foo = function(self, x)
print("hello");
end
}
This method is REALLY simple, possibly too simple, but it worked well for my project.
It's really easy to do class-like OOP in Lua; just put all the 'methods' in the __index field of a metatable:
local myClassMethods = {}
local my_mt = {__index=myClassMethods}
function myClassMethods:func1 (x, y)
-- Do anything
self.x = x + y
self.y = y - x
end
............
function myClass ()
return setmetatable ({x=0,y=0}, my_mt)
Personally, I've never needed inheritance, so the above is enough for me. If it's not enough, you can set a metatable for the methods table:
local mySubClassMethods = setmetatable ({}, {__index=myClassMethods})
local my_mt = {__index=mySubClassMethods}
function mySubClassMethods:func2 (....)
-- Whatever
end
function mySubClass ()
return setmetatable ({....}, my_mt)
update:
There's an error in your updated code:
Router = {};
mt_for_router = {__index=Router}
--Router inherits from ElectronicDevice
Router = setmetatable({},{__index=ElectronicDevice});
Note that you initialize Router, and build mt_for_router from this; but then you reassign Router to a new table, while mt_for_router still points to the original Router.
Replace the Router={} with the Router = setmetatable({},{__index=ElectronicDevice}) (before the mt_for_router initialization).
Your updated code is wordy, but should work. Except, you have a typo that is breaking one of the metatables:
--Modem inherits from ElectronicDevice
Modem = setmetatable({},{__index,ElectronicDevice});
should read
--Modem inherits from ElectronicDevice
Modem = setmetatable({},{__index=ElectronicDevice});
The existing fragment made the Modem metatable be an array where the first element was almost certainly nil (the usual value of _G.__index unless you are using strict.lua or something similar) and the second element is ElectronicDevice.
The Lua Wiki description will make sense after you've grokked metatables a bit more. One thing that helps is to build a little infrastructure to make the usual patterns easier to get right.
I'd also recommend reading the chapter on OOP in PiL. You will want to re-read the chapters on tables and metatables too. Also, I've linked to the online copy of the 1st edition, but owning a copy of the 2nd is highly recommended. There is also a couple of articles in the Lua Gems book that relate. It, too, is recommended.
Another simple approach for subclass
local super = require("your base class")
local newclass = setmetatable( {}, {__index = super } )
local newclass_mt = { __index = newclass }
function newclass.new(...) -- constructor
local self = super.new(...)
return setmetatable( self, newclass_mt )
end
You still can use the functions from superclass even if overwritten
function newclass:dostuff(...)
super.dostuff(self,...)
-- more code here --
end
don't forget to use ONE dot when pass the self to the superclass function