Table of Contents
One of the more powerful features available to FOX applications is drag-and-drop. It's also one of the more complicated to understand. For more background, see the standard FOX documentation on Drag and Drop.
We're going to start by presenting a skeleton application consisting
of a main window widget (a DropSite
instance) that
parents an FXCanvas
widget:
require 'rubygems' require_gem 'fxruby' include Fox class DropSite < FXMainWindow def initialize(anApp) # Initialize base class super(anApp, "Drop Site", nil, nil, DECOR_ALL, 0, 0, 400, 300) # Fill main window with canvas @canvas = FXCanvas.new(self, nil, 0, LAYOUT_FILL_X|LAYOUT_FILL_Y) end def create # Create the main window and canvas super # Show the main window show(PLACEMENT_SCREEN) end end if __FILE__ == $0 FXApp.new("DropSite", "FXRuby") do |theApp| DropSite.new(theApp) theApp.create theApp.run end end
Since the main program (i.e. the part at the end) won't change for
the rest of the tutorial, I won't show that code anymore. Since an
FXCanvas
widget relies on some other object (its
message target) to draw its contents, we need to handle
SEL_PAINT
messages generated by the canvas. We'll do
that by adding a handler that clears the canvas to its current background
color:
require 'rubygems'
require_gem 'fxruby'
include Fox
class DropSite < FXMainWindow
def initialize(anApp)
# Initialize base class
super(anApp, "Drop Site", nil, nil, DECOR_ALL, 0, 0, 400, 300)
# Fill main window with canvas
@canvas = FXCanvas.new(self, nil, 0, LAYOUT_FILL_X|LAYOUT_FILL_Y)
# Handle expose events on the canvas
@canvas.connect(SEL_PAINT) { |sender, sel, event|
FXDCWindow.new(@canvas, event) { |dc|
dc.foreground = @canvas.backColor
dc.fillRectangle(event.rect.x, event.rect.y, event.rect.w, event.rect.h)
}
}
end
def create
# Create the main window and canvas
super
# Show the main window
show(PLACEMENT_SCREEN)
end
end
Run this basic version of the program to be sure that it's working properly so far. You should simply see an empty window with a white background.
Now, on to the fun stuff. Our goal is to be able to drag color data
from some other window, such as an FXColorWell
widget, and drop it onto the canvas in order to change the canvas'
background color. In order for a FOX widget to be able to accept drops at
all, we need to first call its dropEnable()
method:
def initialize(anApp)
# Initialize base class
super(anApp, "Drop Site", nil, nil, DECOR_ALL, 0, 0, 400, 300)
# Fill main window with canvas
@canvas = FXCanvas.new(self, nil, 0, LAYOUT_FILL_X|LAYOUT_FILL_Y)
# Handle expose events on the canvas
@canvas.connect(SEL_PAINT) { |sender, sel, event|
FXDCWindow.new(@canvas, event) { |dc|
dc.foreground = @canvas.backColor
dc.fillRectangle(event.rect.x, event.rect.y, event.rect.w, event.rect.h)
}
}
# Enable canvas for drag-and-drop messages
@canvas.dropEnable
end
At this point, let's try a little test to see if the program does anything interesting yet. Start by running some other FOX or FXRuby program to use as a drag source for the color data. You should be able to use any program that displays an FXColorWell widget, and this includes the standard color dialog box shown here:
Each of the small colored boxes near the bottom of the color dialog box are color wells, and the large box on the left-hand side of the color dialog box is also a color well. Now start your drag-and-drop test program and try to drag a color from one of these color wells onto this window. At this point, the mouse pointer should turn into a stop sign, indicating that the canvas isn't accepting drops of color data yet.
To correct this problem, we need to use the canvas'
acceptDrop()
method to indicate whether or not
we'll accept certain kinds of drops. You can call
acceptDrop()
any time after receiving the initial
SEL_DND_ENTER
message, but it's usually done in
response to a SEL_DND_MOTION
message. Let's add a
handler for SEL_DND_MOTION
messages from the canvas
in DropSite's initialize() method. For now, we'll unconditionally accept
drops from any drag source, regardless of what kind of data they're
offering:
def initialize(anApp)
# Initialize base class
super(anApp, "Drop Site", nil, nil, DECOR_ALL, 0, 0, 400, 300)
# Fill main window with canvas
@canvas = FXCanvas.new(self, nil, 0, LAYOUT_FILL_X|LAYOUT_FILL_Y)
# Handle expose events on the canvas
@canvas.connect(SEL_PAINT) { |sender, sel, event|
FXDCWindow.new(@canvas, event) { |dc|
dc.foreground = @canvas.backColor
dc.fillRectangle(event.rect.x, event.rect.y, event.rect.w, event.rect.h)
}
}
# Enable canvas for drag-and-drop messages
@canvas.dropEnable
# Handle SEL_DND_MOTION messages from the canvas
@canvas.connect(SEL_DND_MOTION) {
# Accept drops unconditionally (for now)
@canvas.acceptDrop
}
end
Now try the previous test again. This time, when you try to drag from a color well to the drop-enabled canvas, you should see the mouse pointer turn into a small filled square. This is a visual cue to the user indicating that the canvas will accept a drop of the drag-and-drop data.
Now it's time to get more specific about what kind of data is being dragged between these applications, and how to process that data. So far, our drop-enabled canvas merely knows that you're dragging some kind of data from a drag source, but it doesn't know what kind of data it is. We really need to be more exclusive about what kinds of data are acceptable. FOX uses unique drag types to distinguish between different kinds of "draggable" data. As you'll see later, you have the freedom to define drag types for any kind of application-specific data that you need; but for now, we're going to use FOX's built-in drag type for color data.
Drag types (even the standard ones) must be registered before they
can be used, and so we'll start by adding a few lines to
DropSite
's create()
method
to register the drag type for color data:
def create
# Create the main window and canvas
super
# Register the drag type for colors
FXWindow.colorType = getApp().registerDragType(FXWindow.colorTypeName)
# Show the main window
show(PLACEMENT_SCREEN)
end
Note that the first time that
registerDragType()
is called for a particular
drag type name (such as FXWindow.colorTypeName
)
it will generate a unique identifier for that drag type. Subsequent calls
to registerDragType()
for the same drag type name
will just return the previously-generated drag type. Now, we want to
modify our SEL_DND_MOTION
handler so that it's a
little more picky about which kinds of drops it will accept:
# Handle SEL_DND_MOTION messages from the canvas
@canvas.connect(SEL_DND_MOTION) {
if @canvas.offeredDNDType?(FROM_DRAGNDROP, FXWindow.colorType)
@canvas.acceptDrop
end
}
Here, we call the canvas' offeredDNDType?
method to ask if the drag source can provide its data in the requested
format. Only if offeredDNDType?
returns true will
we call acceptDrop()
as before.
The last step is to actually handle the drop, and for that we add a
handler for the SEL_DND_DROP
message:
# Handle SEL_DND_DROP message from the canvas
@canvas.connect(SEL_DND_DROP) {
# Try to obtain the data as color values first
data = @canvas.getDNDData(FROM_DRAGNDROP, FXWindow.colorType)
unless data.nil?
# Update canvas background color
@canvas.backColor = Fox.fxdecodeColorData(data)
end
}
Assuming that the drag source is able to provide its data in the
requested format, the getDNDData()
method will
return a String (which for our purposes is just a byte buffer). If you've
defined your own application-specific drag types, this data can of course
be anything, and we'll see examples of this in a later tutorial. But the
data for standard drag types like
FXWindow.colorType
can be decoded using the
appropriate built-in library functions. In this case, we use the
fxdecodeColorData()
method to convert the bytes
into a color value that we can use.
Now comes the moment of truth. Try running your test program again
(one that displays a color well). Now, when you drag a color from a color
well and drop it onto the DropSite
window, the
canvas should change its background color accordingly.
The complete program is listed below, and is included in the
examples
directory under the file
name dropsite.rb
.
require 'rubygems' require_gem 'fxruby' include Fox class DropSite < FXMainWindow def initialize(anApp) # Initialize base class super(anApp, "Drop Site", nil, nil, DECOR_ALL, 0, 0, 400, 300) # Fill main window with canvas @canvas = FXCanvas.new(self, nil, 0, LAYOUT_FILL_X|LAYOUT_FILL_Y) # Handle expose events on the canvas @canvas.connect(SEL_PAINT) { |sender, sel, event| FXDCWindow.new(@canvas, event) { |dc| dc.foreground = @canvas.backColor dc.fillRectangle(event.rect.x, event.rect.y, event.rect.w, event.rect.h) } } # Enable canvas for drag-and-drop messages @canvas.dropEnable # Handle SEL_DND_MOTION messages from the canvas @canvas.connect(SEL_DND_MOTION) { if @canvas.offeredDNDType?(FROM_DRAGNDROP, FXWindow.colorType) @canvas.acceptDrop end } # Handle SEL_DND_DROP message from the canvas @canvas.connect(SEL_DND_DROP) { # Try to obtain the data as color values first data = @canvas.getDNDData(FROM_DRAGNDROP, FXWindow.colorType) unless data.nil? # Update canvas background color @canvas.backColor = Fox.fxdecodeColorData(data) end } end def create # Create the main window and canvas super # Register the drag type for colors FXWindow.colorType = getApp().registerDragType(FXWindow.colorTypeName) # Show the main window show(PLACEMENT_SCREEN) end end if __FILE__ == $0 FXApp.new("DropSite", "FXRuby") do |theApp| DropSite.new(theApp) theApp.create theApp.run end end