Plugin Development with Visual C-sharp

This is a back-up of the WIKI.
Not all links might work
We're working on a new wiki.

Main Page | Recent changes | View source | Page history | Log in / create account |

Printable version | Disclaimers | Privacy policy

This tutorial is meant for people wanting to start writing their own MapWindow GIS Plugin using Microsoft Visual C# 2008 Express Edition. Recently, Microsoft Visual Studio 2010 was released. Anomalies between both versions will be noted* in the tutorial (when found).

The text below supposes a basic knowledge of the C# language and the Visual Studio environment. Code samples should be interpreted, while copy-pasting them might not always work.

Contents

Getting Started

This section takes a look at instantiating a MapWindows GIS plugin. It covers what almost all plugins will consist of in order to get started, independent of the specific goal of the plugin. A good idea is also to have a look at the sample script supplied in MapWindows itself: open MapWindow, go to the Plug-Ins menu, click scripts and toggle C# and Plug-in. Or to use the template.

Making a Class Library

When Visual C# and MapWindow GIS are installed, the first thing to do is to start a new project in Visual C#. Choose Class Library and give the project a name. A class library is created since you'll make a component .dll, i.e. a set of classes which is used by another application, namely the MapWindow.exe. I'll conviniently name mine MapWinPlugin. After clicking OK, wait until Visual C# loads the project. Normally, the solution explorer and the Class1 (which may be renamed to MyPlugin) code window opens.

When renaming things, make a habit of right-clicking the element you want to rename and choose Refactor\Rename... Visual C# will automatically search and rename all the occurences of this element in your code. Note that the class' file name Class1.cs does not automatically adapts to the identity of the class. But that when changing the filename, the user is prompted to change (or not) the class identity.

Making a MenuItem in the Plug-Ins menu

The MyPlugin (or Class1 if you didn't rename it) is the main class of the component MapWinPlugin.dll you'll be making and is the entrance for the executable MapWindow.exe to enter your plugin. The main class needs to implement the MapWindow interface to guarantee MapWindow of some basic functionality of your plugin.

To implement the interface, you first need to add it to the project's references. In the project menu, click Add Reference or in the solution explorer, right click References and select Add Reference. This will open a window in which you can browse (the Browse tab) to the ...\MapWindow directory (default C:\Program Files\MapWindow) and select the MapWinInterfaces.dll (not MapWindow.Interfaces.dll!)

Though not strictly necessary at this point, it might be convenient to directly load the MapWinGIS.ocx since you'll need these to access MapWinGIS classes (shapefiles, grids, layer classes etc.) in your custom methods.

Under .NET tab, the System.Drawing needs to be added too. As you can check later, the MapDragFinished method has a reference to the System.Drawing library and when not added to the project's references, this may cause a compile error.

Now that the references are there, the Interface can be implemented. Type : Mapwindow.Interfaces.IPlugin just after public class MyPlugin. To shorten code typing, you my want to write using MapWindow.Interfaces; on top of the code below the other using ... ; which shortens the implementation to public class MyPlugin : IPlugin. This holds for all code in the MyPlugin class, i.e. everytime you would write MapWindow.Interfaces.etc you can leave out the MapWindow.Interfaces.Components when using it.

To automatically load the interface, right click on the IPlugin text just written and choose Implement Interface\Implement Interface. Visual C# now loads all the properties and methods associated with the interface. These are the minimal properties and methods required by the interface.

Note that Visual C# automatically inserts throw new NotImplementedException(); in each method and property. This can be very bothersome if your plugin is loaded in MapWindow, as each time you perform an action that calls one of the methods or properties, an exception is thrown that produces an error message. It is recommanded to delete them and leave the methods blank between the {}, especially, as you may imagine, the MapMouseMove{} method.

There are however properties that need a content for Mapwindow to load properly. These are the plugin properties which are retrieved, independent of whether the plugin is on or off, when MapWindow is started and the MapWinPlugin.dll is detected in the ...\MapWindow\Plugins directory. These are: Author, BuildDate, Description, Name and Version. Change the get{ throw new NotImplementedException(); } to get { return "string"; } in which the string part is replaced by the content you want the property to have, i.e. in the Author property you put "your name" (the " " need to be there), in the Description a short "description" of what the plugin does and in the Version the current "version". The Name property represents the "name" of the menu item that will appear in the Plug-Ins menu of MapWindow. For BuildDate, either a "fixed date" can be set as in the other examples, but it can also automatically be generated. For this enter get{ return System.DateTime.Now.ToString(); } (note: no " ") in the BuildDate{} property.

Note that all properties need to have a get{} in them, so also the SerialNumber whose get{} may not be empty (compile error: not all code return a value) but can be set to SerialNumber { get { return null; } }. The methods do not have a get{} and can be left blank between the {}.

Your code should look a bit like:

using MapWindow.Interfaces;
//other default using
namespace MapWinPlugin
{

public class MyPlugin : IPlugin
{
public string Author
{
get { return ???John???; }
}
public string BuildDate
{
get { return System.DateTime.Now.ToString(); }
}
public string Description
{
get { return ???This is my Plugin???; }
}
public string Name
{
get { return ???MyPlugin???; }
}
public string SerialNumber
{
get { return null; }
}
public string Version
{
get { return ???test???; }
}
public void OtherMethods(parameters)
{
}
}

}

Compiling

This can be compiled* and run in MapWindow. Do not save your project (Save All) directly in the ...\MapWindow\Plugins directory. A .dll will be created both in the \bin and the \obj directories of your project. Mapwindow can't deal with two .dll with the same name in its Plugins folder and will hence not load the plugin. Save your project elsewhere (default in MyDocuments\Visual Studio 2008\projects\).

First of all, make sure that the target Framework of your project is set to 3.5. You can check the Framework in the project properties (in the project menu) under the Application tab. Default the Target is .NET Framework 4, but this should be set to 3.5 (the one which MapWindow is currently working on). Visual C# will prompt you to reload the project, this is OK. After converting to 3.5, there will be a reference Microsoft.CSharp which is not compatible with 3.5. You can delete this reference, it is not needed.

Now Build Solution F6 in the Debug menu of Visual C#. The .dll component can be found under the ...\projectname\projectname\bin\Release directory, where projectname (default named after the namespace of the library, i.e. MapWinPlugin) is the name you saved your project under, and can be copied to ...\MapWindow\Plugins directory.

One way to avoid needing to copy paste your solution is to change the Output Path in the Build tab of the project's properties (in the menu Project, open the MapWinPlugin properties). Changing the Output Path to the ...\MapWindow\Plugins directory will build (a single copy of) the solution directly in the Plugins folder together with a .pdb file (debug unformation). You can also choose not to create a debug file in the Advanced options of the Build tab. If changing the output path, make sure the Copy Local for the MapWinInterfaces and MapWinGIS references is set to False. This can be done in the properties window of these references. If copy local is true (default), the MapWinInterfaces.dll and MapWinGIS.ocx will be duplicated in the output path together with the MapWinPlugin.dll component you just built.

Make sure when you replace the .dll component, either directly when building a solution or indirectly by copy paste, MapWindow is not running. Otherwise an error will occur that the file is used by another program and cannot be replaced.

Whenever MapWindow is now started, a MyPlugin (or whatever you entered in the Name property) item should appear in the Plug-Ins menu. Clicking on it won't do anything (except maybe generating error messages if you forgot to delete one of the throw new exceptions). To see the results of your programming, in the Plug-Ins menu click Change plug-ins. In the list popping up, your plugin's name should be listen. Highlight it and the plugins properties, i.e. author, build date etc., appear below as you have defined them in the code.

Letting the MenuItem do something

Having made a menu item in the Plug-Ins menu, it doesn't really do anything. Most probably you'll be wanting either of the following three events to occur when someone toggles the plugin:

  1. a menu in the main menu
  2. a toolbar
  3. a windows form

This will be achieved by manipulating three methods of the main class (the one that implements IPlugin):

public void Initialize(IMapWin MapWin, int ParentHandle){}

This method is called when the plugin is turned on in the Plug-Ins menu. Here you'll create a new instance of either a main menu or a toolbar. The IMapWin MapWin parameter is inserted in the plugin so that the MapWindow interface, such as menu items, toolbars, layers, etc. outside the plugin can be manipulate from in your plugin code. I'm not familiar with the ParentHandle

public void Terminate(){}

This method is called, as the name suggests, when the plugin is toggled off. Here you'll destroy objects when they are not destroyed by the automatic garbage collector (variables that are declared within the plugin will be cleaned when the plugin is exited as their life time ends).

public void ItemClicked(string ItemName, ref bool Handled){}

ItemClicked is fired whenever the user clicks an item in a menu or on a toolbar. Each clickable item is refered to by its identity and when clicked on, this identity is inputted into the method as the ItemName parameter. Setting the boolean Handled to true prevents other plugins from receiving this event. This is probably where you'll want to trigger a windows form.

First an access point to MapWindow needs to be passed through in the initialize method so you can manipulate the MapWindow interface from within the plugin. The MapWin parameter in the initialize method is only available within that method. But if you want to access it elsewhere in the plugin, you'll need to make a seperate instance like MyMapWin of the IMapWin at class level. The way to do this is:

Declare a variable of type IMapWin. This is done in the upper part of the main class, just "public class" by typing private IMapWin MyMapWin; or private MapWindow.Interfaces.IMapWin MyMapWin; if MapWindow.Interfaces is not used:

public class MyPlugin : IPlugin
{
private MapWindow.Interfaces.IMapWin MyMapWin;
...

(as discussed further, it might come in handy to declare the IMapWin MyMapWin object differently than private, to allow access from other classes in your plugin apart from the main class)

Second thing is to equate the input parameter MapWin to MyMapWin. This is simply done by writing MyMapWin = MapWin; in the initialize method:

public void Initialize(IMapWin MapWin, int ParentHandle)
{

MyMapWin = MapWin;

}

Now you can manipulate MapWindow through the MyMapWin instance to initiate or destroy menu items, toolbars etc., as will be seen below. Note that you did not make a new copy of MapWin in MyMapWin, but rather that both identities refer to the same memory space (the reference is passed, not the object). This is why, whenever changing something to this memory space through MyMapWin, these changes remain active even if ref MyMapWin is destroyed. Hence, when you change something to the MapWindow interface, these changes will remain even if you're plugin is toggled off. If you want to restore things, you'll have to specify this explicitly in the Terminate method of the MyPlugin class.

Creating a new Menu

Since menus will be referred to on multiple occassions, e.g. in the Initialize, Terminate and ItemClicked methods, it is a good idea to declare variables for them so that, if you want to change the names given to them it is not necessary to change it everywhere you used them. This also avoids typing mistakes, as Visual C# will help you writing the variables and warn if you mistype.

In the same place as you declared MyMapWin, write:

private const string MenuHeading = "Name";
private const string SubMenu1 = "SubName1";
private const string SubMenu2 = "SubName2";

and so forth for each menu item that is needed in your plugin menu. Note that the variables are declared constants. This is not only because they are not meant to be changed by any code in the plugin, but also, as will be seen later, because the switch statement requires constants.

Now that the menu items are declared, they need to be instantiated when someone launches the plugin from the Plug-Ins menu or the Plug-Ins dialog box. In the Initialize method, call the function AddMenu for the object MyMapWin:

public void Initialize(IMapWin MapWin, int ParentHandle)
{

MyMapWin = MapWin;
MyMapWin.Menus.AddMenu(string Name, string ParentMenu, object Picture, string Text);

}

The AddMenu method has 4 parameters: Name (required) is the name the menu will have. Here you enter MenuHeading or SubMenu1. ParentMenu (optional) is the menu item of which name is a submenu. For MenuHeading,this field is left empty. This will place MenuHeading on the menu bar in MapWindow. For SubMenu1 you enter MenuHeading, which puts SubMenu1 in the menu that drops down when clicking MenuHeading in the main menu bar. For SubMenu2 you can either enter MenuHeading again, which will list it together with SubMenu1; or you can enter SubMenu1 to place it in the menu list that will appear when hovering the mouse over SubMenu1. This way you can make a whole hierarchy of menu's.

MyMapWin.Menus.AddMenu(MenuHeading);
MyMapWin.Menus.AddMenu(SubMenu1, MenuHeading);
MyMapWin.Menus.AddMenu(SubMenu2, MenuHeading);

The object Picture (optional) can be used to add an icon on the left of the menu item, much like the icons in the left bar in the menu File. If you don't have a custom image, but want to use MapWindow standard icons, some useful files can be found in the Resources and other folders of the repository. These, I suppose, are already on your machine if you downloaded and installed MapWindow GIS. I don't know, however, how to add them into your Visual C# project. You can download the one you want and add it just like a custom image, however.

To use an custom image, first add the file to the Visual C# project's embedded resources: go to the project's properties in the Project menu and click the Resources tab. If there haven't been added any resources, a blue text will appear that states "the project does not contain a default file. Click here to create one." Click it. In the window that appears, open the Add Resource menu and choose existing file. Browse to the image you want to use and open it. The file, for example MyIcon.png, should appear in the Resources window and a Resources folder will be added to the project map in the resolution explorer. MyIcon.png will have a copy stored in the project directory ...\Projects\projectname\projectname\Resources.

To add the icon in front of the menu item:

MyMapWin.Menus.AddMenu(SubMenu1, MenuHeading, projectname.Properties.Resources.MyIcon);

In my case, projectname is replaced by MapWinPlugin, the name of my project as it can be found in the solution explorer and MyIcon by the name of the icon file as it appears under the Resources folder in the solution explorer, without the extension .png (or .jpg, .gif etc.) and with _ replacing possible spaces (if any).

The string Text parameter will replace the Name as the text appearing in the MenuHeading menu. This is a good way to change the visible text of your menu without having to trace (or letting Visual C# trace) all the places where the menu name's are used as variables.

Now that a MyPlugin menu has been created, you also need to destroy it when your plugin terminates to avoid dangling menu's, i.e. ones that stay in the menu but don't do anything anymore. I've noticed that the menu automatically disappears if the plugin is turned off, so explicitly terminating the menu is not strictly necessary. If you do want to be certain not to waste memory, remove each added menu item:

public void Terminate()
{

MyMapWin.Menus.Remove(string Name);

}

Instead of string Name, write MenuHeading. Repeat the same line for each menu item, i.e. for SubMenu1, SubMenu2 and any others you might have created.

Your code now looks a bit like:

using MapWindow.Interfaces;
//other using
namespace MapWinPlugin
{

public class MyPlugin : IPlugin
{
private IMapWin MyMapWin;
private const string MenuHeading = "MyPlugin";
private const string SubMenu1 = "Project1";
private const string SubMenu2 = "Project2";
public void Initialize(IMapWin MapWin, int ParentHandle)
{
MyMapWin = MapWin;
MyMapWin.Menus.AddMenu(MenuHeading);
MyMapWin.Menus.AddMenu(SubMenu1, MenuHeading, MapWinPlugin.Properties.Resources.MyIcon);
MyMapWin.Menus.AddMenu(SubMenu2, SubMenu1);
}
public void Terminate()
{
MyMapWin.Menus.Remove(MenuHeading);
MyMapWin.Menus.Remove(SubMenu1);
MyMapWin.Menus.Remove(SubMenu2);
}
public void OtherMethods{} // OtherMethods are empty
public string Properties
{
get { return "property"; } // see code sample above
}
}

}

This can be compiled by Debug/Build Solution F6 in the Visual C# menu. If the resulting MapWinPlugin.dll is placed in or build into the .../MapWindow/Plugins directory and Mapwindow started, a menu "MyPlugin" should appear when turning the MyPlugin item on in the Plug-Ins menu or dialog box. Clicking the MyPlugin menu, a menu should roll down with SubName1 and an icon beside it. SubName2 appears when placing the mouse above SubName1.

To set the events launched when clicking one of the menu's you've created, manipulate the ItemClicked method as discussed below under A Form.

A Toolbar

For toolbars, you can, as I did with the menu, declare variables for the toolbar and toolbar button names, i.e. write at class level private const string MyString = "StringName"; for MyButtonName, MyToolbarName and MyText, used below. I will, however, show another way to deal with the multiple occurence of the same string in Initialize, ItemClicked and Terminate without having to adjust the literals in each of these methods. Since toolbars often use icons rather than text, the toolbar and toolbutton names are invisible to the user and hence you, as programmer, are less likely to change the names as they only serve purpose in the code and have otherwise no significance. Some toolbarbuttons can have a text beside the icon or consist only of text. However, the button text can also be changed through setting its text property and hence can be handled on one place and doesn't need repetition on multiple places in your code. So it's save to use a literal rather than a variable for the text which appears.

You do need to declare a ToolbarButton object for each button you want to add to your toolbar. This can be, but, as discussed further on, does not have to be, done on class level, below the class declaration:

public class MyPlugin : MapWindow.Interfaces.IPlugin
{

internal static MapWindow.Interfaces.IMapWin MyMapWin;
private MapWindow.interfaces.ToolbarButton MyButton;
...

}

(the MapWindow.Interfaces. is not necessary if you're using MapWindow.Interfaces;, in the following code examples I will presume you are)

Now, go to the method from which you want to create a toolbar. This can be the Initialize method if the toolbar is supposed to appear if the Plugin is (turned) on, or in the ItemClicked method if you want the toolbar to pop up when an item is clicked, for example a menu item. You'll have to insert some code in either (or both) methods in the line where is written write here, as shown below:

public void Initialize (IMapWin MapWin, int ParentHandle)
{

MyMapWin = MapWin;
write here

}

and/or

public void ItemClicked(string ItemName, int Handle)
{

switch (ItemName)
{
case MenuItemName:
Handle = true;
write here
break;
}

}

You can insert buttons in the default toolbar where are situated buttons like open, add layer, zoom etc. If, as is more likely and more proper, you want your own toolbar, write MyMapWin.Toolbar.AddToolbar("MyToolbarName"); This will create, as you can see when you compile at this stage and run MapWindow, a new toolbar, empty and of standard size, which you can drag around, but nothing much more. The MyMapWin in the code is the variable you declared of type IMapWin. "MyToolbarName" is a string literal, as I said I would use. You can however use a string variable as well, omitting in that case the " ".

Now, add a button by calling the AddButton method. This is not a method of the toolbar you just created, but one of MyMapWin.Toolbar, which governs all toolbars. To specify that the button needs to end up in your toolbar, your best bet would be writing MyButton = MyMapWin.Toolbar.AddButton("MyButtonName","MyToolbarName","","");. The first string, MyButtonName, is the name of the toolbar button, which you'll have to use in the ItemClicked method to define the events triggered whenever the toolbar button MyButton is clicked. The second string, MyToolbarName, is the name of the toolbar into which the button will appear. If left empty, "", the button appears in the default toolbar. The third string, here empty, is the parent button which you can use in case of a drop down list of buttons, much like in the menu you had a parent menu. I'm not familiar with the last string After parameter.

Another option is to use the overload MyButton = MyMapWin.Toolbar.AddButton("MyButtonName","MyToolbarName", bool IsDropDown); which seems to enable or disable a toolbar drop down menu, i.e. a small arrow will appear next to MyButton's text/icon which, when clicked, will make a menu to drop down, like the Add layer/Remove Layer/Remove all Layers toolbar button in the standard MapWindow toolbar. The last parameter, IsDropDown, sounds like governing drop down menu's. Funny enough, if you set it to false the drop down arrow will not disappear and you can add as much sub items as you like.

Note that the AddButton method returns a MapWindow.Interfaces.ToolbarButton object, which was assigned to MyButton. This allows you to manipulate the button properties through the MyButton variable. If you leave out the assignment, a button will appear, but you won't be able to reach it to change the icon, the text, the Tooltip etc. To set the button's properties, use the MyButton instance: MyButton.Text = "";, for example will set the text to empty. If you only want an icon you'll explicitly need to set the text to "" since the default is "MyButtonName". The Text property overrides the button name as the text appearing on screen and if you ever want to change the appearing text, change this property rather the name of the button which, since I didn't use variables, may appear as a literal on multiple places in the code. MyButton.Tooltip = "Click me"; will set the text that appears when hovering the cursor over the button to "Click me", while MyButton.Picture = projectname.Properties.Resources.MyIcon; will assign the MyIcon image to the button. projectname needs to be replaced by the project name as it appears in the solution explorer (normally equal to the namespace of your class) and MyIcon needs to be added first as an embedded resources to the project.

I explained how to embed an image or icon file in the project resource in the menu section of this page. Short: go to the project properties in the project menu, click the Resources tab and Add Resource/Add Existing File. Replace MyIcon in the code above by the name of the file added.

So, in the write here line in the above code, your code can look, as an example, a bit like:

MyMapWin.Toolbar.AddToolbar("MyToolbarName");

MyButton = MyMapWin.Toolbar.AddButton("MyButtonName","MyToolbarName","","");
MyButton.Picture = MapWinPlugin.Properties.Resources.MyIcon;
MyButton.Text = "";
MyButton.Tooltip = "Instant Access";

ToolbarButton MyExtraButton = MyMapWin.Toolbar.AddButton("MyExtraButtonName","MyToolbarName","MyButtonName","");

MapWinPlugin should be replaced by your project's name. Note that I've used an extra button which is declared and assigned in the same line and within the scope of the method rather than on class level, to show the drop down option.

Unlike menus, toolbars won't disappear automatically when the plugin is toggled off. Hence, you'll need to explicitly destroy the created toolbar in the Terminate method of your main plugin class:

public void Terminate()
{

MyMapWin.Toolbar.RemoveButton("MyButtonName");
MyMapWin.Toolbar.RemoveToolbar("MyToolbarName");

}

Destroying the toolbar will automatically get rid of its buttons. However, there is a good reason to explicitly destroy both. If the toolbar is removed and then the user triggers the Itemclicked event to instantiate the toolbar again, the button will not appear together with the second instance of its toolbar. This is probably because the button still exist somewhere as part of the previously removed toolbar and hence wil not be instantiated again as part of the newly created toolbar. You can, if you want, also remove a specific button seperately without destroying whole toolbar by the same RemoveButton("ButtonName") code. Note that the sequence of destruction is important: first the buttons of a toolbar, then the toolbar itself.

Since the remove and add functions are methods of MapWin.Toolbar and you remove/add them using their string "name" rather than the button's identity, you can declare the buttons within the scope of their properties' manipulation (i.e. in the same {} as the call to MyButton.Property), rather than on class level, and still add/remove them in the initialize/terminate methods of the main plugin class. When declaring them within the Initialize method, for example, leave out the private in front of the MapWindow.Interfaces.ToolbarButton type, like I've done in the MapWindow.Interfaces.ToolbarButton MyExtraButton in the example above. MyExtraButton will be destroyed together with MyToolbar and can be called in the Itemclicked method with its name MyExtraButtonName just like MyButtonName, even if not declared on class level. This is because MyExtraButton is not an instance of a class ToolbarButton (you did not state MyButton = new ToolbarButton), rather it is a part of the MyMapWin.Toolbar. You can access MyButton in a differnt scope than its instantiation with ToolbarButton MyButton = MyMapWin.Toolbar.ButtonItem("MyButtonName");.

Toolbars will, unlike forms, automatically take care that only one is present no matter how often the event which created them is called, so there is no need to check whether the toolbar is already there or not when creating a new one. There is always but one toolbar with a given name, multiple toolbars need different names.

Compile using the Debug/Build Solution F6 to play around and check what happens when changing the properties of either the Toolbar or the ToolbarButton.

As said above, to set the events triggered when clicking a button, code the ItemClicked method as discussed below considering that the ItemName parameter will equall "MyButtonName" or other string literals you used (or, in case of variable declaration, a constant string variable holding the name of the toolbar button).

A Form

Eventually, you'll probably want a new windows form to pop up in which users can specify certain setting for the plugin, load in shapefiles or grids, make calculations or perform other functionalities.

Creating a new Form

A Windows Form is a seperate Class which implements the interface Form. To add this class, go to the Project menu or right click your project in the solution explorer and Add Windows Form or Add\Windows Form. A Add New Item window pops up in which Windows Form is already selected. Give your form a name, e.g. PluginForm, and Add. A new class is added to the project in the solution explorer and the form's design view opens with an empty form.

To make this form appear when someone clicks either a menu item or a toolbar button, return to the main class code window and scroll to the ItemClicked method. The ItemClicked has two parameters: string ItemName and ref boolean Handle. If something is clicked, the ItemClicked method is called and ItemName is passed through holding the identity of the item which was clicked on. To trigger a windows form, when clicked, add either an if (ItemName == Identity) {} or a switch(ItemName){case Identity: break;} statement in the ItemClicked method and instantiate a form in either statement. This will create an object of class PluginForm, but it will not appear. To let it appear, call the Show() or ShowDialog() method (the difference being that the code lines after Show execute immediately after the form has been "shown", while the thread will wait until the form is closed before executing the code lines below ShowDialog).

As an example, lets pop up a MyPlugin form when someone clicks on SubMenu1, as defined in Creating a new Menu in the case of using a switch command:

public void ItemClicked(string ItemName, ref boolean Handled)
{

switch(ItemName)
{
case SubMenu1:
Handled = true;
PluginForm MyForm = new PluginForm();
MyForm.Show();
break;
}

}

Note that the PluginForm in the above code is the name you gave to the Windows form class and that MyForm is the identity of a new object of type PluginForm which is instantiated when, in this case, SubMenu1 is clicked.

SubMenu1 is here a constant variable and can only be used in this way when the SubMenu1 variable has been declared (a constant) and given a value (see private const string SubMenu1 = "SubName"; in the beginning of the main class). The same effect can be achieved be stating case "SubName":. But when renaming the SubMenu1, you'll also have to adjust case "SubName" to the new name given. This is avoided by using the constant variable. Submenu1 is (the variable holding) the name of the clicked item, not the text property (i.e. what appears on screen) of the menu or the toolbar button.

I put Handled to true. This is not necessary to make the code work. It will, however, prevent other plugins from receiving this event.

Although I prefer switch statements here, you can achieve the same functionality with an if (here exemplified with the toolbar button created in A Toolbar:

public void ItemClicked(string ItemName, ref boolean Handled)
{

if(ItemName == "MyButtonName")
{
Handled = true;
PluginForm MyForm = new PluginForm();
MyForm.Show();
}

}

You can add cases (or if statements) for other menu items as well as for toolbar buttons. You can also add a case (or if statement) for MenuHeading, if you only need one form and do not require submenus.

Note that each time the item is clicked, a new form will pop up. You may want to avoid this and let the program check that if a form is already open, this one is brought to the front, rather than making a new one. Add the same time, note that the created form(s) will not automatically close when the plugin is turned off. To enable these two things, i.e. allow but one instance and make it disappear upon plugin closure, you'll first need to move the variable declaration to the class level rather than the method level, i.e. in the beginning of the class where you wrote private IMapWin MyMapWin; and private const string MenuHeading = "Name"; etc. you declare: private PluginForm MyForm;. This makes MyForm available to all methods in the main class, also the terminate method in which you can remove the form when the plugin is toggled off: if (MyForm != null) MyForm.Close(); (or MyForm.Dispose();). Notice that MyForm will be null, void, empty as long as no form is assigned. If so, the MyForm.Close() will not work, hence you only need to execute .Close() if MyForm has been assigned, i.e. if it is no longer null.

However, MyForm has not been assigned yet: you told the computer there is a PluginForm object MyForm (which is null), but didn't create it yet. In the ItemClicked, write MyForm = new PluginForm(); in place of PluginForm MyForm = new PluginForm();, which turns a variable declaration (with the variable's type in front) into an assignment (without the type in front). However, you only want to make a new form, if no form is active at the moment, so you enclose it in a if (MyForm == null || MyForm.IsDisposed) statement. The IsDisposed method returns true if a form has been created (and therefore MyForm is no longer null) but the user closed it (by, for example, using the red x at the top of every form). When this is not added, MapWindow will return an error if a form is created, closed and then the item which instantiates a form is clicked again without turning off the plugin and then on. You can also add code to make the form come to the front if hidden behind other windows or to pop up if minimized:

case SubMenu1:

Handled = true;
if (MyForm == null || MyForm.IsDisposed)
{
MyForm = new PluginForm();
MyForm.Show();
}
else if (MyForm.WindowState == System.Windows.Forms.FormWindowState.Minimized)
MyForm.WindowState = System.Windows.Forms.FormWindowState.Normal;
else MyForm.BringToFront();
break;

It is possible, but not recommanded to directly create a form if the plugin is toggled on, without a menu or toolbar in between. It is not recommanded, because if the form is closed, the Plugin needs to be first turned off and then on again to make a new instance of the form. Also, if the plugin is on, a form will be created when starting MapWindow, even if the user did not want to use the plugin. To do this add the MyForm instantiation and .Show() call in the initialize method rather than the ItemClicked.

Manipulating the Form

Once within the windows form subclass, you can design the form and handle events within it exactly like a form in a Windows Form Application. How to do this is profusely described over the net. If you're an absolute beginner with either C# or windows forms in Visual C#, a good place to start would be the Beginner Development Learning Center. A reference for those who are more familiar with Visual C# is the library.

In the following sections, we'll take a closer look at the MapWinGIS library to use shapefiles and grids and to manipulate the MapWindows GIS interface.

Using Shapefiles

This section covers all about using shapefiles.

Start by adding the MapWinGIS.ocx to your project's references (if you haven't done so yet): in the project menu, click Add Reference or in the solution explorer, right click References and select Add Reference. This will open a window in which you can browse (the Browse tab) to the ...\MapWindow directory (default C:\Program Files\MapWindow) and select the MapWinGIS.ocx This allows the Visual C# project access to the MapWinGIS ActiveX Control which contains the class definitions used by MapWindow for, among other, shapefiles. A reference to stdole will be automatically added as well.

A first point of interest concerns the difference between shapefiles, Shapes, Parts and Points. A shapefile contains multiple shapes. In a World shapefile, for example, you'll usually have shapes for each country. It is the Shapefile which can be saved to the disc. Shapes in a single shapefile must be of the same Type: point, line or polygon. A point is defined by its x,y position. A line consists of multiple points connected by straight segments. The sequence in which these points are lined is determined by their Index. A polygon is simply a line for which the first point (Index 0) equals the last point (Index n). A polygon can, however, have holes in it. This is where Parts come in: a "doughnut" polygon has two Parts, one a closed line defining its outer boundary (points rowed clockwise) and another part defining its inner boundary (points arrayed counter-clockwise). A shape can exist outside a shapefile but cannot be stored to disc.

Opening/Displaying/Saving Shapefiles

To open a shapefile, first declare a shapefile: MapWinGIS.Shapefile MyShapefile; When you want to use the shapefile outside the openShapefile method, this declaration is best done at class level. The instantiation MyShapefile = new MapWinGIS.Shapefile(); is defined in the openShapefile method, which would normally be a button_Click or ItemClicked event. If you do not need the shapefile outside the method, you can concatenate declaration and instantiation inside the method.

If you're opening a shapefile from within a windows form, you can drag and drop an OpenFileDialog instance from the toolbox onto the form. This allows you to set the properties within its properties window, rather than using code. For example, to set the title of the dialog, go to the properties window (make sure openFileDialog1 is selected, from here on named openShapeDialog), scroll to the Title property and type in a text. If you do not open it from a form, the toolbox will not show anything to drag, but you can use the following OpenFileDialog openShapeDialog = new OpenFileDialog(); instead and set its properties using code. For example, to change the title appearing on the dialog window, type openShapeDialog.Title = "string";

The property you'll bound to change is the Filter. This defines which file format are allowed to be opened. For a shapefile, MapWinGIS provides a format, so rather than typing something along the lines of "shapefile (.shp)|*.shp" in the Filter Property, code the Filter openShapeDialog.Filter = MyShapefile.CdlgFilter; and possibly add the "All Files (*.*)|(*.*)" for convenience sake. Notice that I called a method of the MyShapefile object. So to use this filter, you need a MapWinGIS.Shapefile MyShapeFile; declaration within the scope of the filter and must have instantiated the MyShapeFile = new MapWinGIS.Shapefile(); before calling its property CdlgFilter. This is normally no problem since you need a shapefile to store the opened file in. However, this is the reason why you can't set this filter in the OpenFileDialog Filter properties window and need to do it by code.

This will not do much yet. To make the dialog pop up, write openShapeDialog.ShowDialog();. Whenever the method is called, a window will pop up allowing the user to browse through his computer and select the file he wants to open. When he clicks open, the dialog disappears and the file path is stored into the openShapeDialog.FileName property. It is the path which is returned, not the file itself. To assign the file to your MyShapefile, it won't do to type MyShapeFile = openShapeDialog.FileName, as this assigns a string to a shapefile. Rather use MyShapefile.Open(openShapeDialog.FileName); which references the MyShapefile variable to the actual file.

Now you can start changing things to the shapefile. For example display it on the MapWindow display. To do so, make sure you have a IMapWin typed object MyMapWin in the scope of the method (if this is not the case, see Making IMapWin object available in other classes). Then you display the shapefile with MyMapWin.Layers.Add(ref MyShapefile, "LayerName"); or directly with the file path: MyMapWin.Layers.Add(openShapeDialog.Filename, "LayerName");

The method that will open and display your shapefile looks a bit like:

public class MyClass
{

MapWindow.Interfaces IMapWin MyMapWin;
MapWinGIS.Shapefile MyShapefile;
private void openShapefile_Click(object sender, EventArgs e)
{
MyShapeFile = new MapWinGIS.Shapefile();
OpenFileDialog openShapeDialog = new OpenFileDialog();
//not necessary if you drag/dropped an OpenFileDialog from the Toolbox
openShapeDialog.Filter = "All Files (*.*)|*.*|" + MyShapeFile.CdlgFilter;
openShapeDialog.FilterIndex = 2;
if (openShapeDialog.ShowDialog() == DialogResult.OK)
{
MyShapeFile.Open(openShapeDialog.FileName);
MyMapWin.Layers.Add(MyShapeFile, System.IO.Path.GetFileNameWithoutExtension(openShapeDialog.FileName));
}
}

}

The above code example adds a DialogResult.OK. This is to ensure that only when the user selected a file and pressed OK, the code in the if-statement is executed.

You might have noticed the FilterIndex = 2. This is to set the second Filter file format as the default (the one that will show in the dialog when opened). The Filter format normally is : "Text1|*.extention1|Text2|*.extention2" where | seperates the text shown from the associated format extention and from the next option. The first, Text1|*extention1 is normally the default format. It is, however, not possibly to begin with MyShapeFilter.CdlgFilter. One must first use the " " extentions, end with a | and then the .CdlgFilters.

The System.IO.Path.GetFileNameWithoutExtension(openShapeDialog.FileName) reduces the filename "C:\Folder\...\ShapefileName.shp" to "ShapefileName".

Note that bool s = MyShapeFile.Open(openShapeDialog.FileName); will return a boolean set to true if Open() successfully opened a shapefile and to false if failed.

Saving a shapefile (say you made the necessary changes to MyShapeFile object and want to save it as a new file, not overwriting the old one) can be done very simular: drag/drop a SaveFileDialog onto the form or declare SaveFileDialog saveShapeDialog = new SaveFileDialog;. Set the Filter with saveShapeDialog.Filter = MyShapeFile.CdlgFilter;. Then, in a if (saveShapeDialog.ShowDialog() == DialogResult.OK) {} statement, call the SaveAs method of the Shapefile you want to save: MyShapeFile.SaveAs(saveShapeDialog.FileName);

This will create three files with the same name but different extensions:

  1. .shp : the mainfile containing geometric presentation of geographic entities
  2. .shx : index file
  3. .dbf : attribute table

Making a Shapefile from scretch

The following describes a way to make a shapefile from input: either a list of x, y coordinates in an array[n,2] (possibly read from a text file or a database) or from clicks on the MapWindow view. Since all shapes basically consists of points (connected by segments to form a line -or not-, closed lines to enclose an area -or not), you'll have to start with defining a set of points. Create a point with MapWinGIS.Point MyPoint = new MapWinGIS.Point(); MyPoint.x = X; MyPoint.y = Y; where X and Y are the coordinates you want the point to have.

When adding points to a shape, it is the reference to the point that is passed into the shape, so that any changes to the point will also affect that point in the shape. Make sure, therefore, when inserting multiple points into the shape by a loop, not only the coordinates of the point are changed but also the object the point refers to.

Thus:

for(int r = 0; r < 10; r++)
{

MyPoint = new Point(); //must be within the loop, not outside
MyPoint.x = X; MyPoint.y = Y;
MyShape.InsertPoint(MyPoint, ref r);

}

Points from Text

To read a text from file use the streamreader.

You can use an OpenFileDialog to let the user search the text file. Then, instantiate a new stream, pass the text file in and read each line:

private MapWinGIS.Shapefile TextToShapefile()
{

OpenFileDialog openTextDialog = new OpenFileDialog();
openTextDialog.Filter = "Text (*.txt)|*.txt|All Files (*.*)|*.*";
if (openTextDialog.ShowDialog == DialogResults.OK)
{
System.IO.StreamReader MyStreamReader = new StreamReader(openText.FileName);
string Line = MyStreamReader.ReadLine();

Each time MyStreamReader.ReadLine is called, the Line will skip to the next line in the text file until the whole text is read (in which case ReadLine returns null). Don't forget to close the stream at the end.

while (MyStreamReader.ReadLine() != null)
{
//do something with Line here
Line = MyStreamReader.ReadLine();
}
MyStreamReader.Close();
}

}

Line is just a string containing a line of text. You need to interpret it to transform it into a point. Say each line consist of an x and y value seperated by a seperator character. First declare a seperator (outside the while luss) as a character array. For example, if you want to use the seperator defined in the computer's language settings: char[] seperator = System.Globalization.NumberFormatInfo.CurrentInfo.NumberGroupSeperator.ToCharArray(); or if you want to declare a specific seperator: e.g. char[] seperator = ",".ToCharArray(); if you like to force comma as seperator.

Then you can make a point and insert it into a shape there where "do something with Line here" is stated in the above sample:

MapWinGIS.Point MyPoint = new MapWinGIS.Point();
try
{

string[] cell = Line.Split(seperator);
MyPoint.x = double.Parse(cell[0]);
MyPoint.y = double.Parse(cell[1]);
MyShape.InsertPoint(MyPoint, ref PointIndex);
PointIndex++;

}
catch (Exception X)
{

System.Windows.Forms.MessageBox.Show("An exception occured: " + X.Message + " at Line: " + Line, "Exception:");

}

Split(seperator) will split the Line using the seperator given into an array of strings (in this example string[] cell). To use white space as seperator, type Split(null). double.Parse(string) will convert the string variable into a double, if possible. If not possible, an exception will be thrown and the thread skips immediately to the catch{} region (which, in this case, will give a message stating the kind of problem that occured and the line which caused the problem). InsertPoint, as discussed in From Points to Polyline/Polygon to Shapefile, inserts the point into a shape.

You can also specify the decimal seperator. First declare a local NumberFormatInfo: System.Globalization.FormatInfo MyFormat = new System Globalization.FormatInfo(); Then give it a decimal seperator: MyFormat.NumberDecimalSeperator = "."; if you want to force a point. The computer language settings (System.Globalization.FormatInfo.CurrentInfo.NumberDecimalSeperator) is the one that is used standard and may differ from the text file seperators. Parse doubles with this decimal seperator as: double.Parse("string", MyFormat);

Points from Clicks

To create points by letting the user click on the place where he/she wants a point, you'll need to code two methods: the method which triggers the possibility of clicking the map and the method which handles the map click event. I will use a toolbar button which, when pressed, allows the user to click the map. This by defining a case in the ItemClicked method of the main class. And the MapMouseDown allows to retreave the x, y coordinates which the user clicked. I've read that it is better to use the MapMouseUp method as it can happen while holding down the mouse button that multiple MouseDowns are send but usually only one MouseUp is send upon releasing the mouse button. This is for you to consider.

Let say we have a toolbarbutton by the name of MyButtonName and the identity MyButton. In the ItemClicked method, make a case and write the following down (in so far its not already there):

public void ItemClicked(string ItemName, ref bool Handled)
{

switch(ItemName)
{
case "MyButtonName":
Handled = true;
MyMapWin.View.CursorMode = MapWinGIS.tkCursorMode.cmNone;
MyMapWin.View.MapCursor = MapWinGIS.tkCursor.crsrCross;
MyButton.Pressed = true;
break;
default:
if (MyMapWin.Toolbar.ButtonItem("MyButtonName") != null)
{
MyButton.Pressed = false;
}
break;
}

}

So, if MyButton is clicked, the above case is called. The first thing done is setting the CursorMode to cmNone. There are 5 tkCursorModes: cmNone, cmPan, cmSelection, cmZoomIn, cmZoomOut. The last four all trigger a standard event, while the first one cmNone, does not. Since you don't want to Zoom In at the same time as selecting a point, setting the CursorMode to cmNone is what you want. The best thing to do, however, would be, or so it seems to me, to define your own CursorMode rather than using the generic cmNone. I've not been able to figure out custom CursorModes, however.

The second statement defines the look of the cursor. MyMapWin is, again, the object of class IMapWin within the current scope. The tkCursor enumaration gives you some choice here: crsrCross is one, which I have taken here simply as example. This statement is not obligatory but when not stated, the cursor look will be whatever it was before.

The MyButton.Pressed line will change the appearance of the toolbar button (it gets a square of a slighly different colour around itself) and can conveniently be used to make sure that the user indeed pressed the button and hence wants to be in the "create point mode".

Now, go to the MapMousDown (or Up) method, which is called every time the map is clicked. public void MapMouseDown(int Button, int Shift, int x, int y, ref bool Handled) has a number of parameters passed into it, as you see. The int x and int y is the location in pixel coordinates which was clicked. The Button parameter gives the mouse button used by the user to execute the click. There are three basic int Button values: 1, 2 and 4, (right mouse button, left and middle) which it might be convenient to define as:

public void MapMouseDown(int Button, int Shift, int x, int y, ref bool Handled)
{

switch(Button)
{
case 1:
//write statements here for the left mouse button
break;
case 2:
//write statements here for the right mouse button
break;
case 4:
//write statements here for the middle mouse button
break;
}

}

In case 1, the left mouse button, make an if-statement. As said, the MapMouseDown method is called every time the map is clicked. So you definitly need an if to control whether something will happen or not. The first logic expression can be MyButton.Pressed == true, which makes sure the user has activated the button. Another logic expression can be MyMapWin.View.CursorMode == MapWinGIS.tkCursorMode.cmNone, which makes sure the user did not change his/her mind and first want to zoom or pan to, for example, have a better view of where to click. However, other functionalities outside zooming, panning or selecting will most likely also have CursorMode cmNone. Try, for example, the measure tool and you'll see that apart from executing your code in the if-statement, it will also start measuring. This is avoided by setting MyButton.Pressed to false whenever another button or menuitem is clicked: you can set pressed to false in a default case of the ItemClicked method. If, however, at some time, MyButton does not exist but the plugin is running, an error will appear in such cases as MapWindow will search for MyButton to set its pressed property to false but will not find any object by that identity. Code if (MyMapWin.Toolbar.ButtonItem("MyButtonName") != null) {} around any call to MyButton will prevent this error, as ButtonItem("MyButtonName") will return null if no toolbar button with the name MyButtonName is found.

In the if-statement, you'll want to capture the x,y-coordinates clicked on. int x and int y are pixel locations. To transform them into project coordinates use the MyMapWin.View.PixelToProj(double PixelX, double PixelY, ref double ProjX, ref double ProjY) call. PixelX, PixelY are where you put x, y in pixels. ProjX, ProjY are passed in by reference, hence their value will change without need for an assignement. Store the ProjX and ProjY, these are the ones you'll want to use.

case 1 of the MapMouseDown can look like:

if (MyMapWin.Toolbar.ButtonItem("MyButtonName") != null)
{

if (MyCrossSectionForm.ScaleToolButton.Pressed && MyMapWin.View.CursorMode == MapWinGIS.tkCursorMode.cmNone)
{
double projx = 0, projy = 0;
MyMapWin.View.PixelToProj(x, y, ref projx, ref projy);
MyPoint.x = projx; MyPoint.y = projy;
MyMapWin.View.Draw.DrawPoint(projx, projy, 3, System.Drawing.Color.Red);
}

}

A visual confirmation of the click was made by a drawing. To make the drawing work, add a new drawing layer first. I've not added this to the MapMousDown code, because it is better to write DrawingHandle = MyMapWin.View.Draw.NewDrawing(MapWinGIS.tkDrawReferenceList.dlSpatiallyReferencedList); in the ItemClicked case "MyButtonName" section. This will make points drawn one after the other part of the same drawing layer, which makes it easier to remove. DrawPoint has a pixel size (set to 3 in the example) and a colour (set to Red). You can also draw a line from one point to another. NewDrawing has a handle, which is used to remove it and needs to declared as an int in a scope for both where it is set (as above) and where the layer is removed (as below). It can be Spatially referenced (the drawing will move with pans/zooms) or Screen referenced (the drawing has a fixed position on the screen).

The right mouse button is normally used to exit the current activity. So in case 2 of the MapMouseDown method write:

MyMapWin.View.MapCursor = MapWinGIS.tkCursor.crsrArrow; if (MyMapWin.Toolbar.ButtonItem("MyButtonName") != null)
{

MyButton.Pressed = false;

}
MyMapWin.View.Draw.ClearDrawing(DrawingHandle)

to set the cursor outlook back to the standard arrow, to unpress the toolbar button and/or to clear the created drawing. Note: you might have a problem when calling ClearDrawing when DrawingHandle has no or an inappropriate value. You can also clear all drawings (MyMapWin.View.Draw.ClearDrawings()).

From Points to Polyline/Polygon to Shapefile

Now that you know how to make points, you might want to combine them into a polyline or polygon.

First declare and instantiate a shape: MapWinGIS.Shape MyShape = new MapWinGIS.Shape(); Then create the shape, giving a type: MyShape.Create(ShpfileType.SHP_POLYLINE);, or any other type, and give it at least one part MyShape.InsertPart(int firstPointIndex, ref PartIndex). firstPointIndex would probably be 0 (the index of the first point). PartIndex is a reference, so you can't just insert a number, but need to use a variable as in int PartIndex = 0; MyShape.InsertPart(0,ref PartIndex); (Note: inserting a part is not necessary for shapes of type point). Now, start to insert your points: MyShape.InsertPoint(Point, ref PointIndex); in which Point is a MapWinGIS.Point object and PointIndex, again, a reference. The sequence in which you insert your points is not important, it is their index which defines the sequence in which they are connected by the line.

If you want to make this polyline a polygon, it is sufficient to change the type of the shape, i.e. set MyShape.ShapeType = ShpfileType.SHP_POLYGON; and insert the first point of the line with an index equal to the number of points in the polyline (equal to, since the Indices start at 0): int Index = MyShape.numPoints; MyShape.InsertPoint(MyShape.get_Point(0), ref Index);. Thus, the first and last point in the row are the same.

A Shape can exist without being part of a Shapefile. However, if you want to save to disc or want to combine multiple shapes, you'll need a shapefile. Declare and create one with MapWinGIS.Shapefile MyShapefile = new MapWinGIS.Shapefile(); MyShapefile.CreateNew("name.shp", MapWinGIS.ShpfileType.SHP_POLYLINE);, in which the type and name is of course to be adapted to your preference. A Shapefile can, however, hold only shapes of the same type. Now insert shapes by using a reference int Index; by typing MyShapeFile.EditInsertShape(MyShape, ref Index); in which MyShape is the MapWinGIS.Shape to be added.

The code below, as an example, should turn an array of points in a polyline shapefile:

int Index = 0;
MapWinGIS.Point[] points;
//an array of points

MapWinGIS.Shape MyShape = new MapWinGIS.Shape();
MyShape.Create(MapWinGIS.ShpfileType.SHP_POLYLINE);
MyShape.InsertPart(0, ref Index);
//the shape to be made

MapWinGIS.Shapefile MyShapeFile = new MapWinGIS.Shapefile();
MyShapeFile.CreateNew("name.shp", MapWinGIS.ShpfileType.SHP_POLYLINE);
//the shapefile

for (int r = 0; r < points.Length; r++)
{

MyShape.InsertPoint(points[r], ref r);

}

MyShapeFile.EditInsertShape(MyShape, ref Index); //insert shape into shapefile

The shapefile is automatically in editting mode after its creation. However, when you open a shapefile from elsewhere, it has to be set in editing mode, else changes to it will not be stored. Use StartEditingShapes and StopEditingShapes methods to intitiate editing, respectively stop editing and save the shapefile.

Adding a Field in an Attribute Table

Now that you know how to make shapefiles with some shapes, you'd like to add information of each shape into the shapefile's attribute table? Create a field and add it to the shapefile. You do not need to add a [[MapWinGIS:Table|table] object to the project, the shapefile's method EditInsertField and EditCellValue directly manipulate the attribute table.

MapWinGIS.Field newField = new MapWinGIS.Field();
int FieldIndex = MyShapeFile.NumFields;

newField.Name = "Heading";
newField.Type = MapWinGIS.FieldType.STRING_FIELD;
newField.Width = 15;
newField.Precision = 0;
//actually not necessary since field type is string

MyShapeFile.EditInsertField(newField, ref FieldIndex);

This will create a field in the attribute table. Apart from string typed fields, you can also add double and integer fields. The property precision only has a meaning for double typed fields.

When inserting shapes into the shapefile, code MyShapefile.EditCellValue(FieldIndex, ShapeIndex, "newVal"); in which ShapeIndex is the index of the shape which you want to correspond to "newVal" (a string in case of a string typed field) value in the field.

For changing things to the attribute table, the shapefile needs to be in editing mode as well. If this is not the case (either because you just created the shapefile or you set StartEditingShapes to true), use the StartEditingTable and StopEditingTable methods.

Working with Grids

Handling Images

This section covers images

MapWinGIS.Image is also to be found in the MapWinGIS.ocx reference, so add it to your project's references (if you haven't done so yet): in the project menu, click Add Reference or in the solution explorer, right click References and select Add Reference and browse to the MapWinGIS.ocx A reference to stdole will be automatically added as well. Note that when using both using MapWinGIS; and using System.Drawing;, the Image class is ambiguous between MapWinGIS.Image class and System.Drawing.Image class.

Opening/Displaying/Saving Images

Opening and displaying images is very similar to opening and displaying shapefiles or grids. See there for more information, here follows a short summary: first declare and instantiate an MapWinGIS.Image object. Then use its .Open("filename") method. You can use a FileDialog to let the user search for the filepath. Use the CdlgFilter to allow all compatible formats (and no other).

MapWinGIS.Image MyImage = new MapWinGIS.Image();

OpenFileDialog openImageDialog = new OpenFileDialog();
openImageDialog.Filter = MyImage.CdlgFilter;
openImageDialog.Title = "Open Image";

if (openImageDialog.ShowDialog() == DialogResults.OK)
{

MyImage.Open(openImageDialog.FileName);
MyMapWin.Layers.Add(ref MyImage, System.IO.Path.GetFileNameWithoutExtension(openImageDialog.FileName));

}

Saving images can be equally simple, but given that there are many formats, you may complicate matters. An image filter can best look like: saveImageDialog.Filter = MyImage.CdlgFilter. This seems to work fine and saves the image in the format selected from all options. MyImage.Save(saveImageDialog.FileName); will save the image in the format as given by the extension in FileName (which is normally added from the filter specified).

A more standard image filter may look like:

saveImageDialog.Filter = "Image Files(*.jpg; *.jpeg; *.gif; *.bmp; *.ppm)|*.jpg; *.jpeg; *.gif; *.bmp; *.ppm|Bitmap Images (*.bmp)|*.bmp|JPEG Images (*.jpg, *.jpeg)|*.jpg;*.jpeg|PPM Images (*.ppm)|*.ppm|GIF Images (*.gif)|*.gif";

It is but a small selection of the available Image Types but trying to save others such as png, tiff or asc will not work. The only for which save methods are defined are bmp, gif, jpg and ppm. The default extension is the same as the image was loaded in, so if nothing else is specified (either by selecting the correct filter or by typing the extension), the image is saved "as is". Overrides such as MyImage.Save(sd.FileName, false, MapWinGIS.ImageType.BITMAP_FILE) do not force the file to be saved as, in this example, .bmp.

The World File

An image is standard added to the mapwindow view with the centre of its lower left corner at project coordinates (0,0) and with a pixel being a square with side 1. If you want to position and/or shape the image differently, you'll have to make a world file. A world file is a simple text document that can be written in an editor like notepad and that specifies in a few lines how the image should be placed within the projection. It consist of a couple of lines:

  1. pixel size in the x-direction
  2. rotation about y-axis
  3. rotation about x-axis
  4. pixel size in the y-direction
  5. x-coordinate of the centre of the upper left pixel
  6. y-coordinate of the centre of the upper left pixel

You can query for the original image properties. A worldfile that represent the default way an image is positioned in Mapwindow would have the following values:

MyImage.GetOriginal_dX()
0
0
-MyImage.GetOriginal_dY()
0
(MyImage.GetOriginalHeight() - 1) * MyImage.GetOriginal_dY();

Note that the fourth line is negative. This is because the mapwindow places the lower left corner at (0,0) and the y-values of the image pixels above 0 (upwards), while the world file supposes the origin in the upper left corner and the y -values of the image pixels below 0 (y axis upwards). This also explains why the fifth value is not 0, but the Height (the number of pixel rows) minus one multiplied by the size of one pixel (which gives the distance from the centre of the lowest to the centre of the upmost pixel).

A world file must have an extension similar to the extension of the image format: replace the last letter with a "w" (i.e. jpg becomes jgw, etc.), and must be saved in the same folder as the image. The world file is read when the image is opened, not when adding it the the view, so if you programmetically change some value, you need to reopen the image (i.e. MyImage.Open("filepath");) before adding it to the view.

You can write a world file programmetically with the StreamWriter. The above world file can be created as:

string filepath = "...\image.jpg";
MyImage.Open(filepath);

System.IO.StreamWriter writer = new System.IO.StreamWriter(filepath.Substring(0, filepath.Length - 1) + "w");<\code>

<code>writer.WriteLine(MyImage.GetOriginal_dX());
writer.WriteLine("0");
writer.WriteLine("0");
writer.WriteLine(-MyImage.GetOriginal_dY());
writer.WriteLine(MyImage.GetOriginalXllCenter);
writer.WriteLine(MyImage.GetOriginalYllCenter + (MyImage.GetOriginalHeight()-1) * MyImage.GetOriginal_dY());

writer.Close();

MyImage.Open(filepath);
MyMapWin.Layers.Add(ref MyImage, System.IO.Path.GetFileNameWithoutExtension(filepath))

Of course, the purpose of the world file is to change the values to position and size the image differently then the default to align images with other elements (shapefiles, grids, other images) in the map.

It is not recommanded to change the image position without writing it to a world file. One could, for example, change the lower left anchor (default 0,0) to x, y coding MyImage.XllCenter = x; MyImage.YllCenter = y; although this will set the bounding box and displace the image, other elements will not work such as ZoomToLayer.

Colour Information

Colour of a pixel is stored as an integer value V = 256*256*blue + 256*green + red with blue, green and red the RGB values. To get the RGB values from a pixel (at row i, column j) in the image:

int V = MyImage.get_Value(i, j);
int r, g, b;
V = System.Math.DivRem(V, 256, out r);
V = System.Math.DivRem(V, 256, out g);
V = System.Math.DivRem(V, 256, out b);

r, g and b should now contain the respective RGB values.

Unfortunately, it is a little bit more complicated than that. The get_Value(int row, int col) is not equivalent to Bitmap.GetPixel(int row, int col), because the row and col parameters are not image pixel coordinates like in the Bitmap method. MapWindow GIS works with different coordinate systems. The two most familiar are: window coordinates, upper left corner of the MyMapWin.View is the (0,0) position and your monitor pixels the advancement; project coordinates are related to the context of the layers in the map and can be, for example in longitude/latitude world coordinates. You can switch between both with the PixelToProj and ProjToPixel methods found under MyMapWin.View.

When images are concerned, one could distinguish a third coordinate system corresponding to the resolution of the colour information in the image. To complicate things MapWinGIS places the (0,0) at the lower left corner of the image and its relation with the other two coordinate systems is regulated by the XllCenter and YllCenter (which give the (0,0) image position in project coordinates), dX and dY (which give the size of a single image pixel in window coordinates) and Width and |Height (which gives the position of the upper right image corner relative to XllCenter and YllCenter). dX, dY, Width and Height change dynamically as you zoom/pan or otherwise position the image elsewhere within the View. Alternatively you also have the Original XllCenter, Original YllCenter, Original_dX, Original_dY, Original Width and Original Height. Those correspond to the one in the World File and are not changed with view changes but are used to position the image relatively to other layers in the project. dX etc. may be equal to Original_dX etc. when the image is not displayed because it lies outside the view.

Unfortunately, get_Value works with neither of the three. Below is some code that switches from one to the other coordinate systems:

MapWinGIS.Image MyImage = new MapWinGIS.Image();
MyImage.Open("C:\\...");
MyMapWin.Layers.Add(ref image);

Now that we have an image loaded (possibly with a WorldFile) and added it to the view (note MyMapWin is the IMapWin object in the scope of this example), suppose you call a MouseUpEvent.

public void MapMouseUp(int Button, int Shift, int x, int y, ref bool Handled)
{

then x, y are the window coordinates which you can switch to project by:

double projX = 0, projY = 0;
MyMapWin.View.PixelToProj(x, y, ref projX, ref projY);

after calling PixelToProj, projX and projY come out holding the projective coordinates. Switching back is similar:

double WinX = 0, WinY = 0;
MyMapWin.View.ProjToPixel(projX, projY, ref WinX, ref WinY);

Note that you need doubles in the ProjToPixel while the original x, y window coordinates were intigers. Now for the image pixels:

int PX = (int)System.Math.Floor((projX - image.GetOriginalXllCenter()) / image.GetOriginal_dX());
int PY = (int)System.Math.Floor(image.GetOriginalHeight() - 1 - (projY - image.GetOriginalYllCenter()) / image.GetOriginal_dY());

If you would have a Bitmap copy of the image, you could now retrieve the colour by calling MyBitmap.GetPixel(PX, PY); which should get you the same colour as the get_Value call below.

int VX = (int)System.Math.Floor((projX - image.XllCenter) / image.dX);
int VY = (int)System.Math.Floor(image.Height - (projY - image.YllCenter) / image.dY);
int V = image.get_Value(VX, VY);

I'm not sure why the above works for get_Value, but it seems to be consistent. You might want to check, however, whether projX and projY are within the image boundaries. Normally get_Value should return -1 when outside the image, but when I tried, I got colour values as if the image was concatenated one after the other in the x-direction (no problem in the y direction).

}

What about Layers?

This section covers layers and a layer.

In a plugin, layers are part of the main MapWindow GIS program, so changes to them persist outside the plugin. They can be found in IMapWin. You'll need a reference to MapWindow.Interfaces, but this should already be there as explained in the early beginning of this page. You'll also need an object reference to an IMapWin object, here named MyMapWin. It is passed into the plugin by the initialize(IMapWin MapWin, ...) method (see Letting the MenuItem do something) and can be made available to other parts of your plugin (see Making IMapWin object available in other classes). Properties and methods associated with the collection of all layers can be accessed then by calling MyMapWin.Layers.Property, respectively by MyMapWin.Layers.method(). Individual layers should be taken out of the enumeration by MyMapWin.Layers[int layerIndex].

There may be some confusing about the difference between the map, the view, a layer and the object in a layer. This is how I see it: a layer controls the way a shapefile, grid or image is displayed in the map. The map then being a composition of all the layers that have been added to it. Lastly, the view is that part of the map that can currently be seen within the bounds of the MapWindow GIS program. Hence the view changes as one pans or zooms to other parts of the map.

Note that there is no Map class in a plugin, but that this is reserved for people adding the ActiveX Control to their own application or web service. the Map class is different from IMapWin, hence, the way layers are handled in a plugin can be quite different from the way they are handled in applications.

Miscellaneous

Debugging in Express Edition

Unfortunately the debug mode won't work in Express Edition. Express Edition does not allow to define an excecutable (such as the MapWindow.exe) as a start up program for your class library (i.e. the .dll component). Hence, the debugger does not know which program is using the plugin component and can't debug. That's why the solution is build into the ...\bin\Release folder and not debugged into the ...\bin\Debug directory, which remains empty.

In the Full Visual Studio version, the start-up program can be defined in the project's properties (in the project menu) under the debug tab.

There are ways around the Express Edition limitations, but I've not figured them out entirely: open the .csproj.user file in your Visual project folder in notepad and adding the lines

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <StartAction>Program</StartAction> <StartProgram>C:\Program Files\MapWindow\MapWindow.exe</StartProgram>

does not seem to entirely do the job.

Making IMapWin object available in other classes

As discussed in the beginning, the IMapWin MapWin parameter in the Initialize method is assigned to a local variable MyMapWin (declared at class level) to make the MapWindow Interface available throughout the main class. Your plugin will, however, most likely contain more classes in which you want to access the interface (the MyMapWin object).

There are several ways to do this:

declare MyMapWin global

declare MyMapWin static

internal static MyMapWin; allows to call the MyMapWin method by using the class name (rather than the object identity) of the class in which the declaration is made (main plugin class, for example).

pass MyMapWin as a parameter

This is what is done in the initialize method of the main plugin: a parameter MapWin is passed through from the method calling the plugin and you can equate MapWin to a variable declared at class level to make the object available to other methods of that class. You can do the same with your custom classes: pass in a IMapWin parameter in the constructor method and equate it to a class variable. Note that it is the reference which is passed, no copy of the object itself is made (i.e. no extra memory usage) and any changes to the object's properties persist (as there is only one object, accessible through different identities).

public partial class SomeClass
{

MapWindow.Interfaces.IMapWin MyMapWin;

public SomeClass (MapWindow.Interfaces.IMapWin MapWin)
{
MyMapWin = MapWin;
}

}

and where you call the SomeClass (ItemClicked in the MyPlugin class, for example) you insert the ref variable to IMapWin into the SomeClassObject = new SomeClass(MyMapWin); initiation.

Using a template plug-in

We've made a template plug-in that you can use to get started. It already adds a menu, a toolbar and a winform. It's is also well documented. Just download it (using tortoiseSVN is the easiest) and rename it. It's made for C# and VS2008 You can get it from TemplatePluginVS2008 That folder also holds an OpenOffice presentation: Building plug-ins for MapWindow GIS.odp to explain how to modify the template.

Using StyleCop

It is recommended to use StyleCop StyleCop analyzes C# source code to enforce a set of style and consistency rules. It can be run from inside of Visual Studio.

Retrieved from "http://mapwindow.org/wiki/index.php/Plugin_Development_with_Visual_C-sharp"

This page has been accessed 4,292 times. This page was last modified on 22 September 2010, at 14:21.