|
|
![]() |
| ||||||||||||||||||||||||||||||||||||||||||
DisclaimerFirst I apologize to all those using VS.NET 2002. The source code solution is in .NET 2003: you should still be able to extract it and open the source in your own VS.NET 2002 solution though. It wasn't my intent to alienate anyone, I just don't have VS.NET 2002! Also this is my first article. I've read Marc Clifton's A Guide to writing articles for Code Project, and what a great article that is... but I ask you to please bear with me, and I hope you like my style! PrereqBefore reading, it would help if you had some knowledge of Windows Forms, Controls, and the usage of the app.config file. If not, you should be able to follow along, but it might get a little rough. Be warned, this is a long article...! Series IntroductionI found myself writing this article because I couldn't find a better one on this subject online! (joke) Seriously, there isn't a great lot of detail on using Windows Forms
Drag and Drop online. What you can find usually doesn't cut it. Most of
the Drag and Drop online is centered around files being dragged to your
application, or I have humbly taken it upon myself to change that and hopefully help others learn more about Windows Forms, Drag and Drop and Custom Controls in general. I am planning several articles in a series on Windows Forms Custom Controls and how to create them (in my humble opinion). This first one, as mentioned, starts with Drag and Drop. I start here, because I find it's easier to create a control when the basic control functionality that all GUI users expect, already exists. This allows you to plug (I hesitate to use that buzz word) your control into an existing UI with little work. The SourceThe source code included with this article is a solution, containing a class library, and the Windows Forms Application to test it. The Class Library (the focus of this article) is heavily commented, while the test application (only about 15 lines) is not. The solution and the Class Library are all under the Drag and Drop BasicsDrag and Drop, in its conception and implementation, is actually very easy. The problem stems from needing to customize the data being dragged/dropped. In order to use Drag and Drop at all, one parent surface (usually a
Form or Panel) must have the Next, you need a way to signify that a Control is being dragged. This
is done in the Control's MyButton.DoDragDrop((Button)MyButton, DragDropEffects.Move); What
this does is tell Windows that a Drag and Drop is beginning, using a
Button (named MyButton ), and using the
DragDropEffects.Move enumeration.
DragDropEffects specifies what type of Drag and Drop
operation this control is capable of. You can Copy, Move, Link, etc... but
I find myself using Move all the time when it comes to a complete control,
unlike just the Text of a ListViewItem . When the
DoDragDrop call is made, your control is saved in an
IDataObject object that we'll look at next.
The next step would be to let the parent surface know a Control is
being dragged to it. This is done in the parent Control's
// DragOver event (e.Data is the IDataObject I eluded too in
// the paragraph above.)
if ( e.Data.GetDataPresent(typeof(Button)) )
{
e.Effect = DragDropEffects.Move;
}
else
{
e.Effect = DragDropEffects.None;
}
The reason this won't work is because of the
Your next question should be... well what is that? You didn't say anything about a data format! I have a feeling you'll be presently surprised at how easy it is to create... but using my Class Library it gets even easier. Let's take a look... Lets get down to it!One of the best features (I think) of .NET is the ease in which a seemingly unrelated class can modify a Control (or another class) through delegates and interfaces. That is the basis for my articles. Each class or Control we develop will modify another class or Control, and do it in just a few lines of code. The end result is a Library that provides functionality to many controls without the hassle of writing the same code over and over. Just imagine... the code above is very small, but if you had to write it over and over again, for each app that needed to support DragDrop... boring at best, and not very efficient! In order to fix this, I created a (I should take a minute here to inform you that Drag and Drop can not be in an MTA Thread Apartment, so making the handler static won't hurt us in that respect.) The The first thing, some terminology: simple and custom
controls. Simple controls are the name I have given to standard controls
that come with Windows Forms. (ex: Button). A Custom control is just what
it sounds like, a control we created. But it has one special element, in
order for a custom control to work with the handler, it must implement the
Lets take a look at the /// Provides an interface for controls that are handled
/// by the DragDropHandler and require additonal functionality.
public interface IDragDropEnabled
{
// Indicates the control is dropping
void DropComplete(bool _successfull);
// Tells the control to store its Location (Left, Top)
void StoreLocation();
// Gets / Sets if the control is being dragged
bool IsDragging { get; set; }
}
It is very straight forward. In fact, in the test app, I don't actually
do anything in the Next let's look at the custom control we use to test this interface in the test app, just so you can see how little code is involved once you use the handler. public class TestControl : System.Windows.Forms.Control,
LAND.ClassLib.DragDrop.IDragDropEnabled
{
bool m_IsDragging = false;
/// Required designer variable.
private System.ComponentModel.Container components = null;
public TestControl()
{
// This call is required by the Windows.Forms Form Designer.
InitializeComponent();
this.Width = 32;
this.Height = 32;
}
/// Gets / Sets if the control is being dragged
public bool IsDragging
{
get { return m_IsDragging; }
set { m_IsDragging = value; }
}
/// Unused but in the interface
public void StoreLocation()
{
}
/// Unused but in the interface
public void DropComplete(bool _successfull)
{
}
/// Clean up any resources being used.
protected override void Dispose( bool disposing )
{
if( disposing )
{
if( components != null )
components.Dispose();
}
base.Dispose( disposing );
}
Component Designer generated code
protected override void OnPaint(PaintEventArgs pe)
{
if ( ! this.IsDragging )
{
SolidBrush brush = new SolidBrush(Color.DarkBlue);
pe.Graphics.FillRectangle(brush, 0, 0, this.Width - 1,
this.Height - 1);
}
// Calling the base class OnPaint
base.OnPaint(pe);
}
}
The only real work done here is the MyButton.DoDragDrop((Button)MyButton, DragDropEffects.Move); you
don't see that do you?
Let me take one more moment to break topic real quick... all the
controls you will see me create are derived from
The actual DragDropHandlerFrom here, we'll develope the actuall handler. Each section will be one method of the handler, until you've seen the whole thing. Lets start with the class members: // The sectionGroup(s)/section(s) that contain DragDropHandler mappings
private const string m_szDFSection =
"LANDClassLib/DragDrop/DataFormats";
private const string m_szDFPSection =
"LANDClassLib/DragDrop/DataFormatParents";
// Collections of DataFormats and DataFormatParents
private static Hashtable m_htDataFormats = new Hashtable(5);
private static Hashtable m_htDFParents = new Hashtable(2);
private static bool m_Initialized = false; // Are we initialized yet?
private static bool m_IsSimpleDrag = false; // Is this a simple drag?
The first thing you should notice are the two strings that refer to "mappings". In order to make this handler as robust as possible, I allow for DataFormats and Drag and Drop Parents to be configured through the app.config file. An example is shown below. <configSections>
<sectionGroup name="LANDClassLib">
<sectionGroup name="DragDrop">
<section name="DataFormats"
type="System.Configuration.NameValueSectionHandler" />
<section name="DataFormatParents"
type="System.Configuration.NameValueSectionHandler" />
</sectionGroup>
</sectionGroup>
</configSections>
<LANDClassLib>
<DragDrop>
<DataFormats>
<add key="Test.TestControl" value="LAND_DF_TESTCTRL" />
<add key="System.Windows.Forms.Button" value="LAND_DF_BUTTON" />
</DataFormats>
<DataFormatParents>
<add key="Test.Form1" value="LAND_DF_TESTCTRL,LAND_DF_BUTTON" />
<add key="System.Windows.Forms.Panel" value="LAND_DF_TESTCTRL" />
</DataFormatParents>
</DragDrop>
</LANDClassLib>
First the The Now back to the final members of the handler: // Collections of DataFormats and DataFormatParents
private static Hashtable m_htDataFormats = new Hashtable(5);
private static Hashtable m_htDFParents = new Hashtable(2);
private static bool m_Initialized = false; // Are we initialized yet?
private static bool m_IsSimpleDrag = false; // Is this a simple drag?
The last two are self-explainatory. The
Method #1: InitializationTime for some actual code! Here is the Initialization function: public static void Initialize()
{
if ( m_Initialized )
return;
int idx = 0;
// Collection of DataFormats from the app.config file
NameValueCollection nvc =
(NameValueCollection)ConfigurationSettings.GetConfig(m_szDFSection);
if ( nvc == null )
throw new Exception(
"Invalid section requested during Initialization."
);
// Store the collection in our internal collection
for ( idx = 0; idx < nvc.Count; idx++ )
{
// Get the custom string for our new DataFormat
// and generate a new DataFormat for it
DataFormats.Format frmt = DataFormats.GetFormat(nvc[idx]);
// Store the new DataFormat by object type
m_htDataFormats.Add(nvc.GetKey(idx), frmt);
}
// Collection of DataFormatParents from the app.config file
nvc = (NameValueCollection) ConfigurationSettings.GetConfig(
m_szDFPSection
);
// Store the collection in our internal collection
for ( idx = 0; idx < nvc.Count; idx++ )
{
// Store the DataFormatParent mappins by parent object type
// The value is a string[] of DataFormats that can be DragDropped
// on the parent
m_htDFParents.Add(nvc.GetKey(idx),
((string)nvc[idx]).Split(new Char[] {','}));
}
m_Initialized = true;
}
Most of that is straight forward, but the interesting bit is the line: DataFormats.Format frmt = DataFormats.GetFormat(nvc[idx]); This
is the part I reffered to when I said we would use the value from
app.config DataFormats to generate a new
DataFormats.Format and use that as the value for our
hashtable, since that is what GetDataPresent requires, not a
string .
Method #2, #3, #4 GetDataFormatThe next set of methods provide the lookup into our hashtable for a
private static string GetDataFormat(string _szType)
{
string szRetVal = "";
// If our custom configuration maps the specified type, return the
// DataFormat, otherwise return ""
if ( m_htDataFormats.ContainsKey(_szType) )
{
// Get the Name of the DataFormat for the specified object Type
szRetVal = ((DataFormats.Format)m_htDataFormats[_szType]).Name;
}
// If the custom DataFormat doesn't exist, throw an exception
if ( szRetVal == "" )
throw new DataFormatException(
string.Format(
"Unable to get DataFormat:\n\nan unmapped control type:\n
{0}\nwas specified.",
_szType));
return szRetVal;
}
The string _szType is determined in the overridden
methods by doing the following: string szType = _ctrl.GetType().ToString();
where Method #5: DeriveUnknownFormatIf you examine the That method can be found below: private static string DeriveUnknownFormat(IDataObject _ido)
{
string szDataFormat = "";
// Determine if the data is a valid type by scanning our
// custom mapped DataFormats and asserting them into
// GetDataPresent until GetDataPresent returns true
// or we exhaust the list
foreach ( object _obj in m_htDataFormats.Values )
{
szDataFormat = ((DataFormats.Format)_obj).Name;
if ( _ido.GetDataPresent(szDataFormat) )
{
break;
}
}
return szDataFormat;
}
Since as you'll see later, the only way to store a control for Drag
and Drop in the DoDragDrop method we talked about, is to use
its DataFormats.Format , we can safely go through our
hashtable of DataFormat.Formats and see if
GetDataPresent recognizes any of them. If it does, we get the
key and we now know the type. If not, this isn't a supported type,
so return nothing.
Method #6: StoreControlThis one is short and sweet. Given a control, use
private static DataObject StoreControl(Control _ctrl, bool _simple_drag)
{
// This is a full DragDrop
m_IsSimpleDrag = _simple_drag;
// Get the custom DataFormat
string szDF = GetDataFormat(_ctrl);
// "Convert" the control to a DataObject using the custom DataFormat
DataObject doControlToDrag = new DataObject(szDF, _ctrl);
return doControlToDrag;
}
Method #7 and #8: BeginDrag and BeginSimpleDrag: Finally getting somewhere!Here is the public static DataObject BeginDrag(Control _ctrl, bool _invalidate)
{
DataObject doControlToDrag = StoreControl(_ctrl, false);
// Inform the control that it has begun a drag operation
((IDragDropEnabled)_ctrl).StoreLocation();
((IDragDropEnabled)_ctrl).IsDragging = true;
// Set the control up to be in the foreground and invalidate it
// incase it needs redrawing
if ( _invalidate )
{
_ctrl.BringToFront();
_ctrl.Invalidate();
}
// return the converted control as a DataObject
return doControlToDrag;
}
And here is the public static DataObject BeginSimpleDrag(Control _ctrl)
{
return StoreControl(_ctrl, true);
}
Not much to either of them huh? The only difference is that
By now I'm sure you're either very confused, or very upset. I have yet to actually show you the elusive line: MyButton.DoDragDrop((Button)MyButton, DragDropEffects.Move);I assure you it's coming, and the rewards of doing it this way will become very clear! Thanks for putting up with me so far :)! Method #9: RegisterControl: The heart (or better, "the brains") of the operationAs I've done above, here is the method, again exceptions were removed. I'll break it up into pieces to explain it better after you get the initial view: public static void RegisterControl(Control _ctrl, bool _parent, bool _simple)
{
// If this is a dragable control and not a parent...
if ( ! _parent )
{
// If the control was registered as a non-simple control but
// does not support IDragDropEnabled, throw an exception
if ( ! _simple && ! (_ctrl is IDragDropEnabled) )
// throw exception here
// Try to get the format of the control
GetDataFormat(_ctrl);
}
// Add the appropriate methods to the control
if ( ! _parent )
{
if ( ! _simple )
{
_ctrl.MouseDown += new MouseEventHandler(_ctrl_MouseDown);
_ctrl.Paint += new PaintEventHandler(_ctrl_Paint);
}
else
{
_ctrl.MouseDown += new MouseEventHandler(_ctrlSimple_MouseDown);
}
}
else
{
// If the parent mappings contain this parent, then
// add the appropriate methods and set the AllowDrop property
if ( m_htDFParents.ContainsKey(_ctrl.GetType().ToString()) )
{
_ctrl.AllowDrop = true;
_ctrl.DragOver += new DragEventHandler(_ctrlParent_DragOver);
_ctrl.DragDrop += new DragEventHandler(_ctrlParent_DragDrop);
}
// Otherwise throw an exception
else
{
// throw exception here
}
}
}
Section 1: public static void RegisterControl(Control _ctrl, bool _parent, bool _simple) The
Control _ctrl is obviously the control to register. What is
registering...? I'll tell you in a second. The bool _parent indicates if the
_ctrl is specified in the DataFormatsParent
section of the app.config or not: false means this control
is defined in the DataFormats section only. bool _simple is one of the places I
thought it read better to use the bool instead of the is
check.
Section 2: // If this is a dragable control and not a parent...
if ( ! _parent )
{
// If the control was registered as a non-simple control but
// does not support IDragDropEnabled, throw an exception
if ( ! _simple && ! (_ctrl is IDragDropEnabled) )
// throw exception here
// Try to get the format of the control
GetDataFormat(_ctrl);
}
If the bool parameters to the function were correct, then we use
Section 3: Here is where things get exciting: // Add the appropriate methods to the control
if ( ! _parent )
{
if ( ! _simple )
{
_ctrl.MouseDown += new MouseEventHandler(_ctrl_MouseDown);
_ctrl.Paint += new PaintEventHandler(_ctrl_Paint);
}
else
{
_ctrl.MouseDown += new MouseEventHandler(_ctrlSimple_MouseDown);
}
}
else
{
// If the parent mappings contain this parent, then
// add the appropriate methods and set the AllowDrop property
if ( m_htDFParents.ContainsKey(_ctrl.GetType().ToString()) )
{
_ctrl.AllowDrop = true;
_ctrl.DragOver += new DragEventHandler(_ctrlParent_DragOver);
_ctrl.DragDrop += new DragEventHandler(_ctrlParent_DragDrop);
}
// Otherwise throw an exception
else
{
// throw exception here
}
}
This is registering (promised I'd tell you). When a control is
registered, depending on its type, event handlers are added to the
control. Custom controls get the Mouse and Paint event handlers while
simple controls only get the Mouse event handler. You'll see these events
later. But look at the parent control... Method #10: CanDropHereI'll show you this one but we won't talk about it. It's very straight
forward: Check the internal static bool CanDropHere(Control _parent, IDataObject _ido)
{
string szDataFormat = ""; // The data format of the control being dragged
string szFoundDF = ""; // The data format that was accepted
szDataFormat = DeriveUnknownFormat(_ido);
// Couldn't find the data in the mappings?... return false
if ( szDataFormat == "" )
return false;
try
{
// Attempt to map the parent type to an allowed DataFormat list
// if it is null or fails, throw an exception
string[] aszAllowedDF =
(string[])m_htDFParents[_parent.GetType().ToString()];
if ( aszAllowedDF != null )
{
// Check each string in the Allowed DataFormats list
// and if it matches the DataFormat from the IDataObject
// then store it and break out of the search
foreach ( string _szDF in aszAllowedDF )
{
if ( _szDF == szDataFormat )
{
szFoundDF = _szDF;
break;
}
}
}
}
catch
{
// throw exception here
}
// If we found the DataFormat return true, this control can be dropped
// here
if ( szFoundDF == szDataFormat )
return true;
// DataFormat was not found so this parent can not host the IDataObject
else
return false;
}
Method #11: GetControl: The last one!public static Control GetControl(IDataObject _ido, bool _dropping,
bool _success)
{
// Get the control using the custom DataFormat
Control ctrl = null;
string szDataFormat = DeriveUnknownFormat(_ido);
if ( szDataFormat != "" )
{
ctrl = (Control)_ido.GetData(szDataFormat);
// If dropping... alert the control unless this is a simple DragDrop
if ( ! m_IsSimpleDrag )
{
if ( _dropping )
{
((IDragDropEnabled)ctrl).IsDragging = false;
((IDragDropEnabled)ctrl).DropComplete(_success);
ctrl.Invalidate();
}
}
}
// Return the control being DragDropped
return ctrl;
}
This method gets the control from the Event handlersAll of the events that are added to controls with a call to
MyButton.DoDragDrop((Button)MyButton, DragDropEffects.Move);
about time huh? private static void _ctrl_MouseDown(object sender, MouseEventArgs e)
{
if ( e.Button == MouseButtons.Left )
((Control)sender).DoDragDrop(
DragDropHandler.BeginDrag((Control)sender, true),
DragDropEffects.Move
);
}
private static void _ctrlSimple_MouseDown(object sender, MouseEventArgs e)
{
if ( e.Button == MouseButtons.Left )
((Control)sender).DoDragDrop(
DragDropHandler.BeginSimpleDrag((Control)sender),
DragDropEffects.Move
);
}
private static void _ctrl_Paint(object sender, PaintEventArgs e)
{
Control ctrl = (Control)sender;
// If we are dragging, then draw a bounding box around the control
if ( ((IDragDropEnabled)ctrl).IsDragging )
{
Graphics grfx = e.Graphics;
Pen pen = new Pen(Color.Black, 1);
pen.DashStyle = DashStyle.Dot;
grfx.DrawRectangle(pen, 0, 0, ctrl.Width - 1, ctrl.Height - 1);
pen = null;
grfx = null;
}
}
private static void _ctrlParent_DragOver(object sender, DragEventArgs e)
{
// If this is a valid control for this parent, allow it to move
// freely over, otherwise disallow DragDrop operations
if ( DragDropHandler.CanDropHere((Control)sender, e.Data) )
{
// cthis... this as a Control
Control cthis = (Control)sender;
// Set the DragDropEffect and get the control we are dragging
e.Effect = DragDropEffects.Move;
Control ctrl = DragDropHandler.GetControl(e.Data, false, true);
// If this isn't an IDragDropEnabled control don't worry about
// showing it's position
if ( ! (ctrl is IDragDropEnabled) )
{
return;
}
// If this control is not part of the current "parent"
// remove it from it's original parent and place it in
// the current parent control collection
if ( ! cthis.Controls.Contains(ctrl) )
{
ctrl.Parent.Controls.Remove(ctrl);
ctrl.Parent = cthis;
cthis.Controls.Add(ctrl);
}
// Set the control being dragged to be positioned to where we
// are dragging
Point NewLocation = cthis.PointToClient(new Point(e.X, e.Y));
ctrl.Left = NewLocation.X + 2;
ctrl.Top = NewLocation.Y + 2;
}
else
{
e.Effect = DragDropEffects.None;
}
}
private static void _ctrlParent_DragDrop(object sender, DragEventArgs e)
{
// Get the control being dropped
Control ctrl = DragDropHandler.GetControl(e.Data, true, true);
// Set the control being dropped to be positioned to where we
// did the drop
Point NewLocation = ((Control)sender).PointToClient(new Point(e.X, e.Y));
ctrl.Left = NewLocation.X + 2;
ctrl.Top = NewLocation.Y + 2;
}
Wrap upThe only thing left is implementation of the
DragDropHandler.Initialize();
DragDropHandler.RegisterControl(this, true, true);
DragDropHandler.RegisterControl(panel1, true, true);
DragDropHandler.RegisterControl(NewCtrl, false, false);
DragDropHandler.RegisterControl(button1, false, true);
Assuming this was done in a form's Load event, the lines mean the following: DragDropHandler.Initialize(); Initialize the
DragDropHandler DragDropHandler.RegisterControl(this, true, true); Register
the form as a parent control DragDropHandler.RegisterControl(panel1, true, true); Register
the panel as a parent control DragDropHandler.RegisterControl(NewCtrl, false, false);Register the NewCtrl as a custom control DragDropHandler.RegisterControl(button1, false, true);Register the Button as a simple control Start the test app, and you can drag and drop the two controls with just the code you see above. DoneThanks for taking the time to read my article. It's been a long road... I hope it was a helpful journey. I'll start working on my second article soon. About Ian Giffen
Other popular C# Controls articles:
|
|
All Topics, C#, .NET >> C# Controls >> General
Updated: 17 Aug 2003 Editor: Chris Maunder |
Article content
copyright Ian Giffen, 2003 everything else Copyright © CodeProject, 1999-2003. Advertise on The Code Project | Privacy |
![]() |
MSDN Communities | ASPAlliance • DevelopersDex • DevGuru • Programmers Heaven • SitePoint • Tek-Tips Forums • TopXML • VisualBuilder • XMLPitstop • ZVON • Search Us! |