Monday, April 16, 2012

Making multiple objects drag-and-droppable

To continue on with the drag and drop topic, we now look at a way to make multiple objects in the gui drag-and-droppable. The idea is simple, rather than focusing on one object, we will be creating a cell array that stores all the handles to the objects that we would like to move, so that depending on where the mouse is, different objects will be the target of the drag-and-drop action. If we don't consider the case where multiple objects overlap, making multiple objects movable is a fairly straightforward procedure. However, to account for the fact that the different objects may overlap, the decision as to which object should be considered first, as well as the implementation of such decision make this problem interesting.

Note that I am making things a little complicated here because my starting point is windowMouseMotionFcn instead of the buttonDownFcn of the specific object. The former detects mouse motion while the latter detects mouse click on the specific object. I could easily start at buttonDownFcn and have the gui behave almost the same way, except that you won't be able to tell if an object is movable unless you click on it, in other words, there won't be any indicator (a hand-shape figure pointer) to indicate that the object is movable. Starting from the windowMouseMotionFcn is, in a way, a walk-around to the missing mouseOver fucntion, which allows you to implement the 'movable indicator'. That said, if you don't care about this functionality, check out selectmoveresize function, which is a MATLAB function that allows you to select/move/resize axes and uicontrol graphic objects.

Key procedures:

1. Create three static text fields and name their tags as lbl_target_1, lbl_target_2, and lbl_target_3. Change their unit to pixel. You can also change their background color to green and fontsize to 16.

2. In the opening function of the gui, create a cell array that consists of all the handles to the static text fields. The order of the handles in the cell array should reflect the order of corresponding uicontrols in the gui, specifically, if you wish to have a particular uicontrol appearing on top of another when they overlap, make sure its handle comes before the handle of the other uicontrol in the array.

movables = {handles.lbl_target_1, handles.lbl_target_2, handles.lbl_target_3};



3. Using uistack function to re-arrange the order of the uicontrols. First move the uicontrol to the top level, then move them down by an order that is equal to their position in the array minus 1. For example, for the third uicontrol, move it to the top first, then move it down by 2.

for i = 1:length(movables)
    cur_target = movables{i};
    uistack(cur_target, 'top'); 
    uistack(cur_target, 'down', i-1);
end


4. Attach drag_and_drop function to the windowMouseMotionFcn, with the argument movables as the cell array that contains the handles to desired uicontrols.

set(hObject, 'WindowButtonMotionFcn', {@drag_and_drop, movables});
5. In the drag_and_drop function, which is called whenever the mouse is moved on top of the gui, use a for-loop to go through all the targets and find out whether the mouse is on top of any of the uicontrol in the supplied handles array. Any time the mouse is found on top of a uicontorl, record the index of the uicontrol in the array and break the loop. Note that since the uicontrols have been sorted based on their "stack order", if there are multiple uicontrols overlap, the one at the top will always be detected.

target_id = [];
% find out which one the mouse is on top of right now 
for i = 1:length(target_handles)
    % get position information of the uicontrol
    lbl_target = target_handles{i};
    bounds = get(lbl_target,'position');
    lx = bounds(1); ly = bounds(2);
    lw = bounds(3); lh = bounds(4);
    if (x >= lx && x <= (lx + lw) && y >= ly && y <= (ly + lh))
        target_id = i;
        break;
    end
end

6. Once the index of the uicontrol is known, use another for-loop to attach grab function to the corresponding buttonDownFcn of the target uicontrol. A for-loop is needed because some uicontrols may require 'resets' if they are shadowed by another higher order uicontrol. .

for i = 1:length(target_handles)
    lbl_target = target_handles{i};
    if target_id == i 
        % set enable to off so that the whole static text field is hotspot
        set(lbl_target, 'enable', 'off');
        set(lbl_target, 'string', 'IN');
        set(lbl_target, 'backgroundcolor', 'red');
        set(lbl_target, 'ButtonDownFcn', @grab);
        setfigptr('hand', handles.fig_mouse);
    else
        % re-enable the uicontrol
        set(lbl_target, 'enable', 'on');
        set(lbl_target,'string', 'OUT');
        set(lbl_target, 'backgroundcolor', 'green');
    end
end
if isempty(target_id)
    setfigptr('arrow', handles.fig_mouse);
end

Video demo:



Files for download

Note: 
1. in the functions attached to the different callbacks of mouse actions in the gui, make sure you are not referring to other uicontrols in the gui by their tags, doing so will limit the use of the functions to the specific gui only. My drag_and_drop function will be a good example of this bad practices, since I  refer to the labels that reflect mouse action/position using their tags. 
2. Static text field isn't really a good choice as the uicontrol to be move around. In future posts in the series, I will switch to axes object instead. 

2 comments:

  1. If a wish to insert a push button in GUI during the execution of the code, for example, I press a button in GUI and then another push button appears, and then I can control and drag this new button .
    Its possible? Any suggestion?

    I'm already thankfull for your support
    Leo

    leoumburana@gmail.com

    ReplyDelete
    Replies
    1. It is tricky because the push button has their own "listener" for the mouse actions, unless you are creating the push button as some other drag-and-dropable object first, and then have it replaced with a real push button once the location is confirmed. That is almost like the GUIDE itself, where you drag-and-drop and push button to the GUI area. Wonder how they do it.

      Anyways, it is probably possible (lol), but likely quite a bit of work. Unless there is some magical functions that I don't know about.

      Delete