Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / artificial-intelligence

Applying Ant Colony Optimization Algorithms to Solve the Traveling Salesman Problem

4.97/5 (50 votes)
13 Sep 2013CPOL58 min read 214.4K   24.6K  
Applying Ant Colony Optimization algorithms to solve the Traveling Salesman Problem.

644067/AppSnapShot.PNG

Application main dialog

Table of Contents

Introduction

I was always interested in Artificial Intelligence problems. So when I saw the article "Genetic and Ant Colony Optimization Algorithms" by Peter Kohout, I immediately downloaded it. The article was about solutions of a Traveling Salesman Problem. While I was reading the article and its code, it occured to me that a math graph might be a natural mathematical model for this problem. Out of pure curiosity, I decided to rewrite the code using the Boost Graph Library. In the process of writing code, I learned about other Artificial Ant algorithms. I decided to add them to the code. I wanted to look deeper into the algorithm's behavior, so I added several statistics to calculate. I added means to present these statistics as graphic charts. I also added means to view and peruse as many graph snapshots at runtime as the user needed. I added serialization of configurations and trip results into XML files, and possibilities to load and run external ant colony configurations and graph snapshots of them. I added the routines to print graph snapshots as tables, and reports for the best to-date solutions. In the end, I had an application with a very different user interface and a very different code.

The application has an open-end architecture. You can very easily add new algorithms and statistics to the application not changing most of the application code.

The AI aficionados will find there possibilities to play with algorithms of Artificial Ant Colony Optimization and their parameters. They will be able to look at every step of the proceedings.

The algorithms implemented are:

  • Ant System
  • Elitist Ant System
  • Ranked Ant System
  • Best-Worst Ant System
  • Min-Max Ant System
  • Ant Colony

These algorithms are discussed below.

I hope that people not interested in AI at all will still find something for themselves in the app code. It might be:

  • Use of the object factory design patterns
  • Use of TypeList templates for registration of objects with factories
  • C++ lambda expressions in STL algorithms
  • Thread synchronization with events
  • Customization of the MFC CPrintDialog class
  • Implementation of a rather complicated user interface

and other things.

Anyway, I wish you good reading.

Acknowledgements

Definitions of Ant Colony Optimization algorithms can be found in the book Ant Colony Optimization by Marco Dorigo and Thomas Stützle. Most of the content of this book is accessible via Google Preview on the book's web page. Also very useful was Ant Colony Optimization for Tree and Hypertree Decompositions, Master Thesis by Thomas Hammerl of Vienna.

I also used ACOTSP Software by Thomas Stützle. This software is written in C under Unix OS. I used it as a reference to Ant Colony Optimization algorithms.

I want to thank Igor Tandetnik for his very useful recommendations in reply to my inquires on MSDN Developers Forums.

Preliminaries

To compile and link the code I used MS Visual Studio 2012 Ultimate Update 3. Because I used my ChartCtrlLib library in the code the project requests an addition of the preprocessor directive _VARIADIC_MAX=10 to the project properties. The debug and release versions of the library are in the Debug and Release folders of the solution directory.

All code can be compiled under MS Visual Studio 2010, but you will have to start a new solution and manually import the project properties from the VS 2012 solution. The source code should not be changed.

In this application, I used the static MFC library ChartCtrlLib.lib, and the proprietary slider control CSliderGdiCtrlT. You can get detailed information on the library from my article An MFC Chart Control with Enhanced User Interface, and about the slider from SliderGdiCtrl: Yet Another Slider Control - Accepts (Almost) All POD Types and Uses GDI+. Headers for both controls and the compiled library are included in the zip files for this project. The part of information about the use of TypeLists is in my article Object Factory Design Pattern: Register Creator Functions Using TypeLists and Template Metaprogramming. Code for the static library and for the slider seamlessly compile under Visual Studio 2012.

You will need the Boost Graph Library. I used Boost v. 1_51.0. You can download it from Boost site or use BoostPro 1.51.0.0 Installer.

All custom controls are built with GDI+.

You will need all this information if you are adding your own algorithms to this application.

The Traveling Salesman Problem

The quote from the "Ant Colony Optimization": The Traveling Salesman Problem is a problem of a salesman who, starting from his hometown, wants to find the shortest tour that takes him through a given set of customer cities and then back home, visiting each customer city exactly once." Each city is accessible from all other cities.

Ant Colony Optimization Algorithms

In all Ant Colony Optimization algorithms, each ant gets a start city. Beginning from this city, the ant chooses the next city according to algorithm rules. After visiting all customer cities exactly once, the ant returns to the start city. The ants might travel concurrently or in sequence. Each ant deposits some amount of pheromone on his path. The amount of pheromone depends on the quality of the ant's path: a shorter path usually results in a greater amount of pheromone. The deposited pheromone suffer from evaporation.

The algorithms use different rules for selection of the next city to move to, for evaporation, and for deposition of pheromone.

A nightmare of each optimization algorithm is to be stuck forever in some locally optimal loop.

Different techniques to escape such loops were developed for different Ant Colony Optimization algorithms.

This application implements six known algorithms so far:

  • Ant System
  • Elitist Ant System
  • Ranked Ant System
  • Best-Worst Ant System
  • Min-Max Ant System
  • Ant Colony

Each algorithm might be run with mutation and/or restart options enabled or disabled.

I did not invent any of these algorithms. I used sources I listed in the Acknowledgments section of this article with minor modifications. Symbols in the algorithm equations below are the same as they are in the sources listed in Acknowledgments.

Ant System

The Ant System Optimization algorithm is the first algorithm proposed and analyzed by Marco Dorigo.

Construct solutions

Initially every path between cities has some initial amount of pheromone. Each ant starts from a randomly assigned city and goes from a city to the next city until all cities are visited exactly once. From the last visited city the ant returns to the start city.

The ant in city i selects the next city to visit by calculating probabilities:

644067/AntSysTrav.PNG

Here

  • sp - partial solution #p
  • N - set of all paths from the city i to all adjacent cities still not visited by the ant
  • cij - path from the city i to the city j
  • p - probability
  • tij - amount of pheromone on the path cij
  • hij - some heuristic factor, usually 644067/AntSysTrav_0.PNG, where dij is a distance between cities i and j, Q is some constant
  • α and β - algorithm parameters.

The ant compares 644067/AntSysTrav_1.PNGfor each j to be traveled to some random number644067/AntSysTrav_2.PNG. If 644067/AntSysTrav_3.PNG , the ant immediately moves to the city j. It means that the ant does not always select the path with maximal pheromone, thus diminishing chances to get caught into a local loop.

There always is a very, very tiny probability of never satisfying the condition644067/AntSysTrav_4.PNG. So in this application I use a modification proposed in Ant Colony Optimization. For the given city i I still generate a random number AntSysTrav_5 but compare it to the moving sumAntSysTrav_6. The sum is updated with each new calculated644067/AntSysTrav_1.PNG. The first j that gives 644067/AntSysTrav_7.PNG is the index of the next city to move. Actually, this modification makes a difference only in the very first trial's steps, when the pheromones deposited on the travel paths are very close to each other. At the end of the test, the ant usually takes the maximum p path.

The ants travel concurrently. It means that there is no pheromone correction until all ants return to their start cities.

Pheromone Update

The ants update pheromones on paths connecting the cities according to the formula:

644067/AntSysTrav_5.PNG

where

  • m - number of ants,
  • 644067/AntSysTrav_11.PNG if the ant k traveled the path 644067/AntSysTrav_13.pngbetween cities i and j; Q is some constant, and Lk is the length of the kth ant's travel
  • 644067/AntSysTrav_12.PNG0 otherwise.

In this application, Q is a city's extent, i.e. the side of the least square that contains all cities. Because Q appears only as a numerator in fractions where denominators are distances, it keeps the results independent of the scale of the city's coordinates.

There are very different recommendations related to the values of α, β, <stromg>ρ, start values of pheromone, and the number of ants (ρ is an evaporation factor, see below). One article recommends α = 1, β=2,  ρ=0.5, and start pheromone t=1/m, where m is the number of cities. Anyway, you can play with them in this application.

It seems that the number of ants is not so important if there is enough number of trials, so set of the start cities for each ant could contain all customers' cities. In this application, we employ only ten ants.

Evaporation

After all ants complete their nth  trip, evaporation is applied to all paths between cities:

644067/AntSysTrav_8.PNG(3)

Here AntSysTrav_9 is the evaporation factor.

There is a question of when to apply global evaporation: immediately after all ants complete their trip, or after all pheromones on graph edges (paths between cities) are updated? Evaporation before updates is, essentially,  increase of updating pheromones.

In literature, some authors apply a (1 - ρ) multiplier to all pheromones, some others do not.

I found that evaporation after updates sometimes prevents the appearance of local non-optimal loops, so in this application global evaporation is applied to graph edges last, after pheromone updates.

Elitist Ant Algorithm

The solution construction and evaporation are as defined by equations (1) and (2) in the Ant System Algorithm.

The pheromone update is a little bit different: on each iteration, the best to date ant deposits an additional quantity of pheromone on paths it traveled:

ElSysTrav_0

Here 644067/ElSysTrav_1.PNG  if the path ij is from Tbs, Tbs is the best to date round trip, e is an algorithm's parameter. It was reported, that the best value for e is between four and six.

Note that this algorithm keeps depositing an additional pheromone on the same paths until the best to date solution is changed. Probably, you might want to try mutations and/or resets with that algorithm.

Rank-Based Ant System

Again, the difference is in the pheromone update.

For each of iterations, the best to date ant and additionally the w-1 best ants for this iteration are selected. The best to date and each of the selected ranked ants deposit pheromone on paths they travel:

644067/RankAntsTrav_0.PNG

644067/RankAntsTrav_1.PNG

Q is a constant, Lr is the length of the rth ant trip, e is the additional multiplier. Again, the best results are at 644067/RankAntsTrav_3.PNG

The member 644067/RankAntsTrav_2.PNG is similar to the Elitist Ant Algorithm parameter for the best to date ant.

Best-Worst Ant Algorithm

I used Analysis of Best-Worst Ant System.. by Oscar Cordon and others. The solution construction is defined by (1), and evaporation rule is as in (3).

Only the best to date ant updates pheromones:

644067/BWASTrav_0.PNG

Here 644067/BWASTrav_1.PNG  if the path 644067/BWASTrav_5.PNG , Tbs is the best to date round trip, Cbs - length of the trip.

In addition, the paths of the round trip of the worst ant for the current iteration that are not in the best to date solution are subject to additional evaporation:

BWASTrav)4.png

Here ρw is an additional evaporation factor for all BWAS_Trav_6.png and 644067/BWASTrav_7.PNG, Tw is the worst solution for the given iteration, and Tbs is the best to date solution.

Usually this algorithm includes a mutation of the pheromone on selected paths of the trail, and the algorithm we resets all pheromones to the initial value and restart search for the best solution when the process seems stuck.

In this application, mutation and restart are options. You can add them to any algorithm.

Min-Max Ant System

The solution construction is according to the equation (1).

There are variants in the selection of the ants allowed to update pheromones: the best to date ant, or the best for current iteration, or the best after latest reset ant, or the best to date ant for even iterations, and the best for iteration for odd iterations.

There are min and max pheromone limits to the quantity of pheromone on the paths between cities, τmin and τmax. It is believed that prevents local loops. So, evaporation is:

644067/MMASTrav_0.PNG

The update is:

644067/MMASTrav_1.PNG

Here 644067/MMASTrav_2.PNG  if the path 644067/MMASTrav_3.PNG  Tsel is a selected ant's round trip, Csel - is the length of the trip.

Some authors initiate pheromones to τmax, but in this application I am using τ0 = 1/ncities.

Ant Colony System

In all previous algorithms pheromones were updated only after all ants completed travel for a given iteration. Ant Colony System is different. After an ant moves from one city to the next, the pheromone on the traveled path is reduced by some value. The solution construction is also different.

Solution Construction

The algorithm has a parameter644067/ACSTrav_0.PNG. The parameter sets the boundary between two rules to select the next city. On each step, the algorithm generates a random number644067/ACSTrav_1.PNG. If644067/ACSTrav_6.PNG, the next city to move is selected according to (1); else index j of the next city to move is:

ACSTrav_2..png

where

  • i is the index of the current city,
  • j is the index of the city to move next,
  • 644067/ACSTrav_3.PNG is a set of cities adjacent to the current city i at step k,
  • τil is the amount of pheromone on the path cil,
  • 644067/ACSTrav_7.PNG,  Q is a constant (in this app it is a city's extent), dil is a distance between the cities i and l,
  • β - algorithm parameter

After the ant moves from the city i to city j, the pheromone on the path cij is evaporated and updated according to

644067/ACSTrav_5.PNG

where

  • ξ -the local evaporation factor,
  • τ0 - additional pheromone (an algorithm parameter).

After the first ant has made the kth step and has modified a pheromone, it does not begin the next step. It waits until all other ants complete their kth steps and modify the pheromones on their traveled kth path. In this process, the n + 1 ant waits for the update by the ant n, ant and so on.

Evaporation and pheromone update

After all ants have completed iteration, pheromone update and global evaporation are applied to paths.

In Ant Colony Optimization for Tree and Hypertree Decompositions only the best to date ant is allowed to update pheromone. Evaporation also is allowed only on paths visited by this ant.

In this application, you have a choice: the beast for iteration ant, the best to date ant, the best after reset ant, or all ants.

The application uses equations (3) for evaporation and (2) or (4) for the global pheromone update.

The equations are applied to paths visited by the selected ant(s) only.

Pheromone Reset (Restart)

If ants are stuck in some local loop, there is an option to reset all pheromones to the start values and continue run. This option might be enabled or disabled for all implemented algorithms.

The selection threshold 644067/RST_0.PNG is a reset parameter.  If

644067/RST_1.PNG

all pheromones on paths between all cities are reset to 644067/RST_2.PNG, and search for the shortest path begins anew. Still, the previous history is not destroyed. The new best to date trip updates the best after reset value; if it is less than the global best to date trip, it also replaces the old best to date value.

In (12)

  • 644067/RST_3.PNG is a reset parameter,
  • m is the number of paths in ant's round trip,
  • 644067/RST_4.PNG - number of different edges for a current iteration between the trajectories of the best to date and the worst for iteration ants,
  • 644067/RST_5.PNG - number of consecutive events when 644067/RST_6.PNG ,
  • 644067/RST_7.PNG is a threshold, the reset parameter.

Essentially, the reset occurs when the best to date and the worst for iteration trajectories are very close. The user can select the action after reset: finish the run or continue it.

If the run is to continue, the count of consecutive events 644067/RST_8.PNG resumes from zero.

Mutation

A mutation of the pheromones on ant paths is used to prevent the solution from getting stuck in some local loop. It is explained in the article "Analysis of Best-Worst Ant System.." by Oscar Cordon and others.

It seems that it was proposed for the Best-Worst Ant Algorithm only.

In this application, the global mutation option might be enabled or disabled for each of the implemented algorithms.

The idea is simple: you select ant paths (graph edges) at random and increase or decrease the amount of pheromone on each of them according to some set of rules. The rule to evaporate or increase pheromones is:

644067/Mut_0.PNG

where

  • 644067/Mut_7.pngis pheromone on the edge 644067/Mut_1.PNG, 644067/Mut_8.png is a subset of paths between cities selected at random,
  • 644067/Mut_2.PNG is a random value generated for each path Mut_1.png second use,
  • 644067/Mut_4.PNG - add-subtract threshold, the parameter
  • 644067/Mut_3.png  - min and max limits of pheromones on paths
  • 644067/Mut_9.PNG 
  • 644067/Mut_10.png - mutation strength, the mutation parameter,
  • 644067/Mut_11.PNG - average pheromone on path of the best to date round trip,
  • 644067/Mut_12.png  - number of current iteration,
  • 644067/Mut_13.png - number of iteration from the last pheromone reset (see below),
  • 644067/Mut_14.png - max number of iterations (run parameter)

The number of paths in 644067/Mut_16.pngis calculated as

644067/Mut_15.png

where

  • 644067/Mut_6.PNG - mutation rate, parameter,
  • m- number of cities

The user sets a period (interval) between mutations. In addition, the user can set a number of resets to delay the start of the mutation period. For example, if the number of resets is set to three and the period of mutation is set to 30, then the application will wait for three resets to occur in a row before it starts to count iterations for mutation.

After 30th iteration, it will execute the mutation. After that, the application will wait for another three resets to repeat the count. If the new reset occurs in the middle of the count, the iteration counter will be reset. If the number of resets is set to zero, the mutations will be executed every 30 iterations.

Pay attention to the fact that the degree of the pheromone correction increases at the end of the test.

Travel Statistics

You can gather these statistics:

  • Best for iteration and best to date trip length values at each iteration (always is ON)
  • Worst for iteration and worst to date trip length values at each iteration
  • Average over ants travel length and its standard deviation at each iteration
  • Distance - average and standard deviation of number of paths between cities in ants round trips different from paths in the best to date solution at each iteration
  • Average and standard deviation of the branch factor - number of paths with the quantity of pheromone deposited on them greater than some threshold value
  • Distribution of the ants start cities
  • Best after reset value of round trip for each iteration

The average and standard deviation of some random variable ν are calculated as:

644067/Stat_0.PNG

644067/Stat_1.PNG

where

  • 644067/Stat_2.PNG is an average of n variables,
  • 644067/Stat_3.PNG is their standard deviation.

Distance and brunch factor need some additional explanation.

Travel Statistics: Distance

For each ant in the given iteration, the ant's round trip is compared to the trip of the best to date ant. Paths unique for the ant trip are counted. After that, average and standard deviation of counts over number of ants are calculated. This statistics is a measure of the quality of the trip: the low distance means that either the solution is found, or we have a local loop.

But how will you count the unique paths on a symmetrical graph? The symmetrical graph has path A->B equal to path B->A. We decided that direction does not matter: if the best to date ant has traveled clockwise, and the other ant traveled counterclockwise the same trajectory, these ants traveled the same trajectory. Pay attention to it.

Travel Statistics: Branch Factor

Let us assume we have n cities our ants have to visit. Then each city has n-1 adjacent cities to choose from for a next step. Each of the paths to the cities has some quantity of pheromone deposited on it.

For each city, Branch Factor Statistics calculates the number of paths to adjacent cities where the quantity of the deposited pheromone is greater than some threshold. The less the count, the more deterministic is a choice, the better is a solution.

The branch threshold is calculated for each city separately as

644067/Stat_4.PNG

where

  • 644067/Stat_5.PNG - max and min pheromones on outgoing paths for the city k,
  • 644067/Stat_6.PNG - parameter set by the user

Note that the branch factor takes into account all adjacent edges, no matter if they were visited or not.

Presentation of statistics

All statistics except the start cities are presented as linear charts. The start cities are rendered as a bar graph.

The first two charts, the best for the iteration and the best to date, and start cities are shown in read-only window in the app's main dialog. The user might invoke a separate window to analyze, save, or print all linear charts.

User Manual (Sort Of)

It is a rather long chapter, but the user interface is not so simple.

To run some algorithm this sequence of actions should be performed:

  1. Select an algorithm by name
  2. Select the method of city generation
  3. Set the cities extent that defines the range of city coordinates
  4. Set the number of cities
  5. Set the max number of iterations
  6. Set parameters α, β, and ρ (equations (1) and (3).)
  7. Set other algorithm's parameters if appropriate
  8. Enable or disable mutations and reset and set their parameters
  9. Set or reset symmetry options of ant paths if applicable
  10. Select statistics to gather
  11. Set up a trigger to get a travel graph snapshot if desired

Steps 1 - 10 might be performed in any order and many times: the selections will be committed only after you click the 'Apply' button. Even if the selections are committed, you still might change them if you did not start running the algorithm.

After 'Apply' is clicked, you can run the algorithm step by step or continuously, stop and resume the run, or cancel the run at any moment. The algorithm parameters are locked after you start the run.

While the algorithm is running, it displays the state of the ants' travel every 100th epoch, at each stop and at the end of the run.

While the algorithm is running or at the stops, you can view the charts of the gathered statistics and work with these charts in a separate dialog box. Upon satisfaction of the trigger condition, the app will show a graph snapshot in a separate window.

You can save a city configuration and/or a city graph structure with its vertices and edges into XML files.

For the iteration(s) of your choice, you also can save ant states and the underlying graphs of the solution, statistics charts, and the graph snapshot as XML files.

At any time you can also print the report - best to date trip data registered at that time.

All user actions and requests submitted while the algorithm is running are performed at the end of the current iteration. After the request is honored, the algorithm continues the run.

To change the algorithm or its parameters, you have to cancel run (button 'Cancel') or wait to the end of the run.

Let us go to the details.

Select an algorithm

It is simple: just select an item in the 'Algorithm' combo box.

Set cities extent, city number, max iteration number, α, β, and ρ

Use appropriate sliders. To coarse set values use mouse, for fine adjustment use Home, End, Page Up, Page Down, Image 74 and Image 75 keys.

Select the method of city generation

The application offers five methods of city generation:

  • 'Automatic Placement': the cities are placed on a circle. The circle diameter is equal to a city's extent.
  • 'Random': X and Y coordinates of cities are generated at random; special precautions are taken to prevent generation of two cities at the same coordinates. The distribution of the coordinate values is uniform on Image 76, Image 77 where Q is the cities' extent.
  • 'Manual Placement': You generate automatic or random placement, or use the existing placement and move the cities as you need. Coordinates of the moving city are shown in the status bar.
  • 'Load Cities from File': Loads coordinates of the set of the cities from an external XML file. You do not need to click 'Apply' to start: go straight to 'Run'.
  • 'Load Configuration from File': This option loads all cities with their coordinates and distances between the cities as a graph. The resulting graph acquires α, β, evaporation factor ρ, the algorithm type, and algorithm parameters from the user's selections in the application's main dialog window. The initial value of pheromones is set as Q/cities number, where Q is a city's extent. Again, you do not need to click 'Apply' to start: go straight to 'Run'.
  • 'Load Graph from File': It imports the snapshot of the algorithm's graph as it was saved at some previous epoch or many days ago: the cities, distances between them, deposited pheromones, all things. If the graph was saved with the charts and start city's bar graph (it is a default option at saving graph in this app), the charts and bar graph are loaded too. You might continue from the iteration (epoch) the graph was saved. To use only the configuration, you have to select 'Use existing configuration' and reapply it. Again, you do not need to click 'Apply'.
  • 'Use existing map': It uses what city's configuration was generated last or loaded.

The options Load Configuration from File and Load Graph from File are using the distances between cities as they were saved in the corresponding XML files. All other options automatically calculate the distances between the generated or loaded cities as Euclidean Distances.

644067/Man_0.PNG

Manually Setting Cities

As was mentioned above, you can manually set any city configuration you want by using Manual Placement. The only constraint is all Xs and Ys must be non-negative. It is recommended to set all other algorithm parameters before you start the manual placement, because the configuration you would set will be committed automatically. Of course, you can set any parameter after you have done with placement with Use existing map.

To do manual placement:

  • Set the sliders 'Cities Extent' and 'Number Of Cities' to values you need.
  • Select 'Manual Placement' in 'Cities/Method of Generation' combo box. The dialog box will be displayed to select the method of cities generation. Select 'Automatic', or 'Random', or 'Using Existing Map', and click 'OK'.
  • The generated cities configuration will be displayed in the main app dialog box's picture control. Select the city with left mouse click and drag the city with mouse or using the arrow keys to the position you want. The city's name and coordinates are displayed in the first pane (pane 0) of the status bar of the dialog box. You also can invoke a city label by right-clicking on the city.
  • When you are done, double left click on the button OK in the picture control. The configuration will be committed, and you can start running.

Remember that the distances between the cities will be automatically calculated as Euclidean distances. If your distances are supposed to be different, see next paragraph.

Setting arbitrary cities configuration and distances (committing city map)

Distances between cities are used in calculations only, so you can set any distance value convenient to you. Be realistic and remember that the shortest possible distance between cities is the Euclidian distance.

Suppose you have a map of places you have to visit, with one-way streets between them. It means that sometimes a distance between places A and B is not equal to the distance between B and A. Still, you can work with your map in this app using an XML configuration file to set the map and load it into the application.

To set an arbitrary cities configuration, you have to generate some start configuration, save it into an XML file, change the file, and load it back.

First, you have to generate a city configuration for your cities extent and number of cities. Any generation method ("Regular', 'Random', or 'Manual') is good. Commit the configuration by clicking on Apply. After that go to the Options/Save/Save Configuration submenu item, click on it, and save the configuration as a configuration graph XML file.

Now open the file in any XML editor (I recommend Microsoft XML Notepad 2007, it is free and good, but you can use MS Visual Studio or anything that works). You will see something like this:

XML
<Config_Graph>
<Graph_Prop CitiesNum="10" SymMut="true" SymDist="false" StartPher="0.1" CitiesExtent="25"/>
<Vertex ID="0" Name="City_0" X="8.536407" Y="3.749408">
<Edges>
  <Target ID="1" Name="City_1" X="2.025207" Y="9.906549" Dist="8.9613676071167"/>
  <Target ID="2" Name="City_2" X="0.2259159" Y="0.8545399" Dist="8.80025672912598"/>
  <Target ID="3" Name="City_3" X="11.42984" Y="5.034723" Dist="3.16607141494751"/>
  <Target ID="4" Name="City_4" X="11.51926" Y="7.236289" Dist="4.58865690231323"/>
  <Target ID="5" Name="City_5" X="6.953999" Y="7.392624" Dist="3.97203135490417"/>
  ....................................................................................................
                                                 
</Edges>
</Vertex>
<Vertex ID="1" Name="City_1" X="2.025207" Y="9.906549">
<Edges>
................................................................................................................

Change the city names, coordinates, and distances between cities according to your map. If distance (A, B) distance (B, A) for some pair of cities, change SymMut and SymDist to false (I will explain it later.)  SymMut must be true for a symmetrical map, where distance (A, B) ≠ distance (B, A) for all cities. Save the file, return to or start the app, and load the configuration.

By default, cities have names like 'City_0', 'City_1', etc. Apply the same procedure to the cities or configuration XML files to change the city names.

Set other algorithm parameters

Click on 'Edit Ant Options'. The appropriate dialog box will be displayed if there are additional algorithm parameters. Select parameters and click OK. If you call this dialog box and do not want to change previous settings, click Cancel. For example, this is a dialog box to set the number of ranked ants and their evaporation factor for Ranked Ants algorithm.

644067/Manual_1.PNG

Enable or disable mutation and/or reset and set their parameters

Click the Options menu button and click the Mutation or Reset menu item. Set parameters in dialog boxes displayed and click OK. If the checkbox 'Allow' in the dialog boxes is unchecked, all other controls of the box are disabled, do not try to change their state then. This is the mutation parameters dialog box:

644067/Manual_2.PNG

Some parameters need explanation (see equations (13)):

  • 'MUT PERIOD' - period of mutations, might be set between 1 and 100 epochs
  • 'SEL THRESHOLD' - the threshold between decreasing and increasing mutations
  • 'MUT RATE' - a fraction of the graph edges subject to mutation
  • 'WAIT FOR RST' - number of resets before start of mutations; could be zero; if the reset is not enabled, the slider is disabled
  • 'MUT STRENGTH' - fraction of calculated full mutation to apply to edges

For Reset, there are only two parameters: the fraction of graph edges unique for the worst ant for the current iteration compared to the best to date trip to invoke the reset, and a number of successive repetitions of this event to reset the travel. There also is a list box to select the actions upon reset. The actions are:

  • No Pheromone Reset: This action is used together with mutations: if mutations are enabled and Wait for Rst not zero, instead of resetting all graph edges to the start pheromone, the application will mutate pheromones after a set number of resets occur.
  • Reset: Upon reset, all pheromones are reset to the start value.
  • End Run: Pheromones are reset and test is finished. The user might continue this test or start a new test.

Set or Reset a Symmetrical Option

If the distance between cities A and B is not equal to the distance between B and A, the cities graph is asymmetrical. You have to calculate pheromones on each edge of the graph separately.

If d (A, B) == d (B, A), there are two options. You may assume that the route between A and B has two separate one-way lanes and calculate pheromones for each lane separately, or assume that there is one two-way lane, and just set the pheromone on edge B, A equal to pheromone on edge A, B. You have a choice for the symmetrical graph to set one of these options: click the Options menu button and click on the Symmetrical Paths menu item.

The only way to set different 644067/Manual_9.PNG distances is via Configuration XML (see Setting arbitrary cities configurations above).

Select Statistics to Gather

There are statistics you can gather:

  • Best for iteration and best to date travel values for each iteration (always ON)
  • Worst for iteration and worst to date travel values for each iteration
  • Average travel length and its standard deviation for each iteration
  • Average and standard deviation of number of paths in ants round trips different from path in the best to date solution for each iteration
  • Average and standard deviation of the branch factor: number of paths for that quantity of pheromone deposited is greater than some threshold value
  • Distribution of the ants' start cities
  • Best after reset value of round trip for each iteration

To enable or disable statistics, click on the Options menu button, click the Set Statistics & Results menu item, and select the desired set of statistics. Use the slider to set the Branch Factor threshold.

For every statistics ('Start Cities' excluded) the collected data is represented as linear charts.

644067/Manual_3.PNG

Run the Test

To commit all parameters, click on the Apply button. The buttons Run and Step are enabled after that.

You can go step by step or run the test continuously and stop the run at any moment. To switch from running to step-by-step you have to stop the run first. To resume the run after stop, you have to click on the Stop button again (capture is changed to 'Resume') after stop.

While the test is running, all controls which allow changing the algorithm, algorithm's and test's parameters, are disabled. You have to wait until completion of the test or interrupt it at any moment with a click on the button Cancel Run. This action only cancels the mode; all parameters are not changed or deleted. You can continue the test with Run or Step from the point you have canceled the test or change the algorithm and/or parameters and click Apply to clear old parameters and data and start with a new set of parameters. Until the new settings are committed, all previous test data are accessible. You can monitor the test's progress all the time until a new algorithm and/or algorithm's parameters are committed.

644067/Manual_8.PNG

Monitoring the test

Every 100 iterations and on each stop the application updates its data and displays the trip's length and its trajectory for the current epoch. It also provides trip lengths for the best and worst ants for the current iteration, best trip after the last reset, and best and worst to date results. You select the ant from the combo box and see an epoch, an ant's ID, and trip length in the control to the left of the box. The trip trajectory is shown below the box.

The statistics are also updated on 100th iterations, on each stop, on completing or canceling the run.  

Statistics charts for the best for each iteration and the best to date trip length are shown in the main dialog. This chart control is read-only; that means that no user input is enabled. To see all charts and interact with them (e.g., hide, pan, zoom, save, etc.) you have to select View Charts from Options. This action will display the Chart control.

644067/Manual_4.PNG

To get a full grasp of the chart control features you better read my article An MFC Chart Control with Enhanced User Interface but here is a short version of its user's manual:

  • To see chart values, enable tracking by a mouse middle button click (the cursor will change its shape to a cross). After that select the X-coordinate and left click on the chart to see the data legend for the selected X.
  • To zoom horizontally, select the zoom boundaries with SHIFT + left click.
  • To zoom vertically, select the first zoom boundary with the SHIFT + left double click and the second boundary with SHIFT + left click.
  • To pan horizontally use left and right arrow keys or SHIFT + mouse wheel.
  • To pan vertically use UP and DOWN arrow keys.
  • To change the Y scale for a selected chart, first select is with CTRL + left click, after that use CTRL + mouse wheel or up/down keys to change the Y-scale
  • All other actions on charts, like hide/show, save, print, are allowed from the popup menu that is displayed on the right mouse click on the chart control.

The size of this chart control window might be changed by the user.

The algorithm continues to run if it was not stopped by the user. To update the charts in this control you have to click View Charts again.

At any moment, you can print the data for the best to date travel for this epoch by selecting the 'Print Report' item from the menu button 'Options'. You will present with MFC Print Dialog to select printer, printer setting, and print options. The report consists of several pages. The first page shows the best to date trip data (travel length, epoch, etc.), the algorithm info, and the picture of the trip trajectory like shown above. Other pages show the best to date trajectory as a table. One page of the table shows approximately 40 rows.

If you select 'Selection' in the print dialog, only the first page is printed. Otherwise the full report is printed. To get more flexibility you can install some converter to print the report to a pdf file (like doPdf, it is free and very good) or print to MS XPS file.

The last and, might be, the most important feature of this app (I did not see it in other Ant Colony apps) is a possibility to set a trigger point and get a system snapshot at this point. The trigger might be set at any time after the algorithm is committed. From the 'Options', select 'Graph Snapshot Settings'. You will see the 'Triggers for Graph Snapshots' dialog box.

644067/Manual_5.PNG

It is self-explaining; only two check boxes need additional mention. The first is Continue Run. By default, the trigger interrupts the test run, and the user must click 'Resume' button to continue the test. If this box is checked, the run will continue.

The second is 'Renew...' Again, by default, the trigger is cleared after a triggering event occurs. When the box is checked, the trigger will reset. For example, if the trigger is set for 'Best to Date Change', you will continuously receive snapshots each time the best to date travel distance is changed. The triggers must be set only after the test parameters are committed, because a click on the button 'Apply' resets them to no trigger mode.

Upon the trigger event, the edges dialog box is displayed. It shows the graph edges originating from some source vertex (a city.) You can change the source city by selecting the new city with left mouse button click on it. You can choose the target vertex for this source city (the target city) with CTRL + left click. A right click on a city will invoke the tool tip window with the city's name. The second right click on the same city will hide the name. The right click on the white space will hide all tooltips.

Text boxes to the right of the edge picture control show main algorithm parameters, data on the selected ant, and data on the source and the target cities and an edge connecting them. The legend for the edge colors is below the edge picture control.

644067/Manual_6.PNG

A click on the 'View Ant Graph' will show an edge view of the snapshot. It has many pages. The first page is a selected (in the previous dialog) ant's travel trajectory. The other page show edges for all source cities; each source city has a separate page. The table shows the target, the distance from the source, the edge pheromone before the ant choose a next edge to travel, probability of choice, and the pheromone after update at the end of the trip.

To navigate between pages, use the buttons at the bottom of page. You also can select the source city by left click on the city on the first page. You also can follow the ant's trip by selecting the start city on the first page, and clicking on the target city circle on the next pages.

 If a page cannot display all edges for the given source city, you may scroll the page with UP/DOWN keys if the page has focus (click on page to set it.)

You can save all tables for all source cities into an XML file. You can also print the all tables, the table for the selected source city, or any number of source cities. Keep in mind that the printing of all ten-city sources will take 11 paper pages; for 100 cities it will take 301 paper pages.

644067/Manual_7.PNG

Saving results in XML files

There are five options to save data in XML files:

  • Save Cities: Saves ID, names, and coordinates of generated cities.
  • Save Configuration: In addition, saves distances between cities
  • Save (Colony) Graph: Saves entire system graph with cities ID, names, coordinates; distances between the cities and volume of pheromone on edges for the epoch the data are taken. The algorithm parameters, mutation and reset parameters, type of gathered statistics are also saved. Also saved are ants' trip trajectories for the given epoch. In addition, the epochs, edge pheromones, and traveled state are saved for the best for last iteration, best to date, best after reset, worst for last iteration, and worst to date ants. Charts and start cities distribution are automatically saved in separate files
  • Save Colony Ants: Saves trip trajectories for all ants
  • Save Charts: Saves statistics' charts

All options are accessible from 'Options/Save'.

In addition, you can save the selected ant's graph from the Edge View dialog, and save charts from a chart control's popup menu.

The cities and configuration might be loaded back to the application via the combo box 'Method of Generation' to run with any registered algorithm. The cities XML file allows modifying cities coordinates and names of the cities before loading the file back. Distances between the cities the application will calculate as Euclidian distances.

The configuration file allows changing the distances between its cities in addition to the cities coordinates and names.

The colony graph XML file encapsulates a full system snapshot at the epoch the saving operation was requested. Actually, it might be three XML files: the graph itself, with algorithm, its parameters, and statistics requested, a charts XML file if the charts have data (if the epoch at saving the graph was greater than zero), and a cities bar graph if the start cities statistics was requested and gathered. This is why I recommend organizing some common folder to keep all three files. You set the name of the graph file only; the chart and cities files are named automatically. For example, if the graph file is named 'TestB.xml, the chart file will have name 'TestB_Charts.xml', and the cities file is 'TestB_CitiesChart.xml'.

If you choose to load the colony graph XML file, the files for charts and cities will be loaded automatically if they exist. After the file(s) are loaded, you can run the test from the epoch the colony graph was saved. If you have lost the chart file the application will gather and calculate statistics beginning from the loaded epoch. The same is true for the start cities distribution.

Upon receiving a request to save into an XML file, the application displays MFC CFileDialog dialog box. You are free to select any place to save the file. To introduce some order in this process, I set default subfolders for each type of data in the AntCol working directory to keep the files, so the CFileDialog opens at the default subfolders for each type of the data to save if you set these subfolders. The subfolders are:

  • Cities
  • ConfigGraphs
  • Graphs
  • Ants
  • Charts
  • AntGraphs

It works as follows:

  • The application gets the full module name of AntCol.exe like "C:\...\AntCol.exe".
  • It strips the module name of the file name (AntCol.exe).
  • It searches the module path for a subdirectory "AntCol".
  • It erases all characters after "AntCol", adds the back slash after "Col", and adds the folder name from the list, depending on what the user wants to save.
  • It passes the resulting path to CFileDialog as a default path, so the dialog starts in this path.
  • If the folder AntCol is not found, CFileDialog will start in the MyDocument directory.

You have to set your working directory as ".....\AntCol" and copy the executable file AntCol.exe into it. After that, you have to create these subfolders in the working directory AntCol.

Of course, you can live without all this stuff and keep saving the XML files in any place on your computer or your network.

What is under the hood, or points of interest

This application is a dialog-based MFC application with heavy use of Boost Graph Library and GDI+.

Its main class, CAntColApp is derived from MFC extension class CWinAppEx.

If you do want to peruse the app code, there are four functions to start with:

  • void CAntColDlg::OnBnClickedBtnapply() processes and commits user inputs to prepare algorithm for running
  • void CAntColDlg::OnBnClickedBtnrun() starts a worker thread to execute the algorithm
  • static unsigned _stdcall CAntColDlg::RunASTravel(void*
    colDataPtr)
    - the worker function
  • LRESULT CAntColDlg::OnTravelOnce(WPARAM wParam, LPARAM lParam) - a handler of the messages the worker thread is sending to the application main thread

All these functions are in the files AntColDlg.h, AntColDlg.cpp.

From there you can travel to other functions called from inside, and the functions these secondary functions called, and so on and so forth.

Model

The natural math model of the Traveling Salesman Problem is a graph: vertices are cities, and edges are routes between the cities. Each vertex is connected to all other cities. In the Boost Graph Library, this model is best described by a directed adjacency list graph. To connect algorithm parameters, cities, and routes between cities to the graph, I have provided bundled properties for the graph itself, its vertices, and edges. The properties are defined in the file Colony.h.

The Graph property is:

C++
struct AntColProp
{
    ..............................................................
    bool m_bSymMut;       // Enabled or disabled ability to change m_bSym
    bool m_bSym;          // If true for edges ij and ji have equal  //distances
    int m_grID;           // Actually index of the associated ant
    int m_startIdx;       // Start point; idx is embedded in vertex  //property
    double m_startPher;   // Start pheromone; used for reinitialization //only
    double m_alpha;       // Power of the pheromone
    double m_beta;        // Power of the distance (edge weight)
    double m_evap;        // Global Evaporation factor; pheromone // attenuation is 1 - m_evap
    float m_citiesExtent; // Cities placement dimension
    string_t m_szAlgName; // Name of the algorithm
};

Here string_t stays for std::basic_string<TCHAR>.

In the directed adjacency list the edge connecting vertices i and j is not equal to the edge connecting j and i. However, more often than not, a route connecting any two cities is bidirectional. The property members m_bSymMut and m_bSym are the flags signaling that additional care should be taken of the symmetrical edges ij and ji to make edge length and pheromone equal. For an asymmetrical graph, m_bSymMut is always false and value of m_bSym does not matter. For a symmetrical graph, m_bSymMut = true, and for bidirectional routes m_bSym must be true. You can try to process edges in the symmetrical graph as asymmetrical by setting m_bSym = false.

The structure also includes a constructor and a function string_t GetAntColPropStr(int citiesNum) to translate member values into string.

A problem with access to the adjacency graph bundled property was reported earlier. The solution is to call boost::get_property(graph&, graph name) which is different from dealing with other bundled properties.

The vertex property is:

C++
struct City 
{
  ......................
  int m_idx;
  // City index
  string_t m_szName;
  bool m_bVisited;
  Gdiplus::PointF m_pntLocationF;
};

By default, upon placement of cities, the cities get names like "City_0", "City_1", etc. Cities names can be changed outside the application and the entire configuration loaded back. The user can set the extent of the square that contain the cities from 1.0 to 100.0.

The edge property is:

C++
struct Path
{
  ..............................
  bool m_bTraveled;
  double m_dist;
  double m_pher;
};

By default the edge length (the distance between two cities), m_dist, is calculated as:

644067/Model_0.png

Again, there is an option to change the edge length out of the application. Because m_dist is used for calculation only, it might have any value.

Here is the definition of the Traveling Salesman's graph:

C++
typedef adjacency_list<vecS, vecS, directedS, City, Path,
           property<graph_name_t, AntColProp> > GRAPH;

Information about this class can be got from the Boost::adjacency_list web page.

I am aware that the STL lists are not the best structure for search, but in the Release configuration and one hundred cities, the app performance is not so bad.

Classes

There are three main players in this application: the classes CAnt, CColony, and the hierarchy of algorithm classes.

CAnt (Colony.h) is just storage to keep all things the ant needs for graph navigation:

C++
class CAnt
{
  ..............................................
  public:
    int m_antID;
    GRAPH m_antCitiesGraph;
    std::list<City> m_lstAntPath;
    double m_travelLength;
    vertex_descriptor m_currVertex;
    vertex_descriptor m_startVertex;
    int m_epoch;
};

CColony (Colony.h) is charged with some tasks to process and keep results of ant trips and some tasks on a set of ants.

C++
class CColony
{
................................................................
public:
  static std::mt19937 m_rndGenAnts; // Mercer generator for cities
  static std::mt19937 m_rndColony;  // To select start cities
  
  static const int m_antNmb; // Ten ants for now
  MAP_ANTS m_mapAnts; 
                             // We keep ants in map antIdx/ant 
  GRAPH m_colGraph;          // Colony Graph; passed to the 
                             // every ant before tripitEpoch;
                             // Initial epoch; zero or loaded from graph
  int m_startRstEpoch;  // Epoch of last reset
  int m_rstNmb;         // Number of resets so far
  int m_maxRun;
};

The main dialog has a member m_antColony. Before each trip, the colony passes the colony graph to its ants. The ant trajectories are saved in m_lstAntPath for each ant, and the colony graph is updated according to the trip trajectories. All ants are kept in a map m_mapAnts.

We will discuss algorithm classes later.

How it works

The user chooses an algorithm, its parameters, number of cities and number of trips, and a method of city generation. He/she also selects the statistics to gather. After that, the user commits the selections by clicking on the button Apply.

The application calls the algorithm factory to instantiate the chosen algorithm and the statistics factory to compile the map of statistics. It also enables/disables the appropriate UI controls.

After that, the user clicks Ran or Step, and the application starts the worker thread to run the algorithm.

The application controls interaction between the main thread and the worker with four event handles (files AntColDlg.h and AntColDlg.cpp):

C++
HANDLE m_hEvAbortRun;
HANDLE m_hEvStopReq;
HANDLE m_hEvInfoReq;
HANDLE m_hEvContRun;

The handle names are self-explaining (more details follow). The events control the wait function in the worker thread. After the worker thread exits or is interrupted by these events, the user might resume the run or change an algorithm and/or algorithm and test's parameters.

The algorithm classes

All algorithm classes (files TravelAlg.h and TravelAlg.cpp) are derived from the base class CASTravelBase. All new algorithm classes must be derived from this base class. The class defines the algorithm interface:

C++
class CASTravelBase
{
public:
  CASTravelBase(); 
  virtual ~CASTravelBase();
  virtual unsigned int TravelGraphOnce(CColony& antColony) = 0;
  virtual void UpdateASGraphOnce(CColony& antColony) = 0;
  virtual string_t GetAlgName(void)const  = 0;
  virtual void GetAlgParams(V_ALGPARAMS& vAlgParams) = 0;
  virtual string_t GetAlgParamStrT(void) = 0;
  virtual void SetAlgParams(V_ALGPARAMS& vAlgParams) = 0;
  static void GetMutParams(V_ALGPARAMS& vAlgParams);
  static string_t GetMutParamStrT(bool bShort = true);
  static void GetRstParams(V_ALGPARAMS& vAlgParams);
  static string_t GetRstParamStrT(bool bShort = true);
  static void SetMutParams(V_ALGPARAMS& vAlgParams);
  static void SetRstParams(V_ALGPARAMS& vAlgParams);
// From WrapBase
  virtual string_t GetParamString(void) {return string_t(_T(""));}
// Reinitialize the edges' pheromones to the start pheromone
  RST_ACTIONS RestartAlgorithm(CColony& antColony);
  void MutatePheromones(CColony& antColony);
protected:
// Evaporate pheromones on all edges
  void EvaporateEgdePheromones(CColony& antColony, double evapFactor);
    void TravelGraphOnce(CAnt& ant);
  void UpdateAntPathPher(GRAPH& colGraph, CAnt& ant);
  void UpdateAntPathPher(GRAPH& colGraph, CAnt& ant, double multiplier);
public:
  static CDlgMut* GetMutDlg(bool bDelete = false);
  static CDlgRst* GetRstDlg(bool bDelete = false);
  virtual INT_PTR RunDlg(void) 
  {  
    AfxMessageBox(_T("There are no additional alg params"));
    return IDCANCEL;
  }
  static void DeleteParamsDlgs(void);
public:
  static double m_mutMinPher;         // for mutation, to prevent -INV and 
  static double m_mutMaxPher;         // For MMAS and just in case
  static bool m_bMutAllowed;
  static bool m_bRstAllowed;
  static int m_mutPeriod;
  static double m_mutRndTrsh;
  static double m_mutRate;
  static double m_mutStrength;
  static int m_mutRstTrsh;  // Nmb of resets before mut period starts
  static int m_mutRstCnt;   // Count of resets
  static int m_rstTrsh;
  static int m_rstCntTrsh; // Repetition of the distance between worst and    
                           // best to date below threshold
  static RST_ACTIONS m_rstAction;    // What to do after that: restart or 
                                     // finish
// Names of parameters
  static string_t m_strMutAllowed;
  static string_t m_strMutMinPher;
  static string_t m_strMutMaxPher;
  static string_t m_strRstAllowed;
  static string_t m_strMutPeriod;
  static string_t m_strMutRndTrsh;
  static string_t m_strMutRate;
  static string_t m_strMutStrength;
  static string_t m_strMutRstTrsh;
  static string_t m_strMutRstCnt;
  static string_t m_strRstTrsh;
  static string_t m_strRstCntTrsh;
  static string_t m_strRstAction;
  static string_t m_strActions[];
};

Strictly speaking, only TravelGraphOnce and UpdateASGraphOnce implement an algorithm itself. All other virtual functions define interactions between the application, the user, and the algorithm.

Generally, all the algorithm must know is its parameters α, β, and evaporation factor ρ (see eq. 1 and 3). It needs them to calculate probabilities of transition to the next city and evaporate pheromones. The algorithm receives these parameters from the colony instance, as the colony graph's properties.

Other parameters, if there are any (e.g., number of ranked ants for Ranked Ants Algorithm, or local evaporation factor for Ant Colony) the algorithm should get from outside. In this application, the user sets these additional parameters from the appropriate dialog boxes. The dialog boxes are separate classes, but the algorithms (the user) might invoke them using the virtual function INT_PTR RunDlg(void). This function just calls DoModal on the appropriate dialog class.

The dialogs do not have their own data members; they use data members of the corresponding algorithms. All algorithms' additional parameters are declared static, so they are accessible any time. On OK, the dialog box reads the state of its controls and sets the appropriate algorithm's static data members.

Each additional algorithm's parameter has a counterpart, its name, a static string. We need the names to facilitate exchanges between XML files and algorithm parameters. We have introduced the virtual functions GetAlgParams(V_ALGPARAMS & vAlgParams) and SetAlgParams(V_ALGPARAMS& vAlgParams). The vector V_ALGPARAMS is a vector of pairs

std::pair(bstr_t (parameter
name) ,variant_t (parameter value))
. Using bstr_t and variant_t allows to have the same type of vector elements for different types of values (e.g., float, int, bool, etc.)

Finally, to render the algorithm state in a legible form, there are the virtual functions GetAlgName, GetAlgParamStrT, GetParamString. The last function is used to set text in the static control of the main application dialog.

The class CASTravelBase also includes static functions to set and get parameters of mutation and reset modes. We have mentioned before that mutation and reset modes might be set by the user for any algorithm, therefore the functions

RST_ACTIONS
RestartAlgorithm(CColony& antColony)
and void MutatePheromones(CColony& antColony) are made members of the base class.

To show how the algorithm interface is defined for some concrete algorithm, I will show the class for the Elitist Ant Algorithm:

There is a class for the Elitist Ant Colony Algorithm derived from CASTravelBase:

C++
class CASETravel:public CASTravelBase
{
public:
  CASETravel(void);
  virtual ~CASETravel(void);
  static CASTravelBase* CreateASETravelAlg() { return new CASETravel;}
  virtual unsigned int TravelGraphOnce(CColony& antColony);
  virtual void UpdateASGraphOnce(CColony& antColony);
  virtual string_t GetAlgName(void) const {return AlgName();}
  virtual void GetAlgParams(V_ALGPARAMS& vAlgParams);
  virtual string_t GetAlgParamStrT(void);
  virtual void SetAlgParams(V_ALGPARAMS& vAlgParams);
  static bool RegisterWithFactory(void);
public:
  virtual CString GetParamString(void);
private:
  unsigned int TravelASEGraphOnce(CColony& antColony);
  void UpdateASEPheromone(CColony& antColony);
  string_t AlgName(void) const {return m_szAlgName;}
public:
  static CDlgEAS* GetMnDlg(bool bDelete = false);
  virtual INT_PTR RunDlg(void) {return GetMnDlg()->DoModal();}
  static void DeleteParamsDlgs(void);
// Members
public:
  static double m_easFactor;
  static string_t m_strEasFactor;
public:
  static const string_t m_szAlgName;
...................................................................
};

You can see an implementation of this class in TravelAlg.cpp.

Algorithm factory

To get instances of algorithms I am using the algorithm factory. It is implemented as a Scott Meyers Singleton:

C++
CTravelAlgFactory& CTravelAlgFactory::Instance()
{
  static CTravelAlgFactory factory;
  return factory;
}

I know about problems with Singleton in multithreading applications. In this application, the worker thread never ever tries to instantiate an algorithm. Instead, it receives the pointer to the algorithm instance from the main thread.

To register the algorithms with the factory I am using a std::map with algorithm names as keys and algorithm creator functions as values. It means that the algorithm must have a unique name for this application.

Each algorithm has a static creator member function, for example:

C++
static CASTravelBase* CreateACSTravelAlg() { return new CACSTravel;}

To register an algorithm, the algorithm factory has the function:

C++
bool RegisterCreatorAlgFn(const string_t& algName, TravelAlgCreator creatorFn)
{
    bool bResAlg  = m_algMap.insert(MAP_TRAVELALG::value_type(algName, creatorFn)).second;
    if (bResAlg)
      m_vAlgNames.push_back(algName);
    return bResAlg;
}

The vector m_vAlgNames is used to initiate a combo box of algorithm names (we will discuss it later).

Each algorithm calls this function to register itself. For example:

C++
bool CACSTravel::RegisterWithFactory(void)
{
  return travelAlgFactory.RegisterCreatorAlgFn(m_szAlgName, CreateACSTravelAlg);
}

If some other algorithm was registered under the same name, the insert function will fail, and the register function will return false.

To register all algorithms in a given application and to have the possibility of easy addition of new algorithms to the application I use TypeLists to enumerate and register all algorithm classes with the factory.

Using TypeLists for algorithm registration

The code for TypeList templates is in the file TypeLists.h.

Andrei Alexandrescu in his book "Modern C++ Design" has defined TypeList as a template.

C++
template <class T, class U>
struct TypeList
{
  typedef T Head;
  typedef U Tail;
};

It is a very simple template, but it is amazing what you can accomplish with it.

In the book and in the Loki library (Loki\MSVC\1300\Typelist.h) there are macros to define TypeLists for sets of classes:

C++
#define TYPELIST_1(T1) TypeList<T1, NullType>
#define TYPELIST_2(T1, T2) TypeList<T1, TYPELIST_1(T2)>
#define TYPELIST_3(T1, T2, T3) TypeList<T1, TYPELIST_2(T2, T3)>
#define TYPELIST_4(T1, T2, T3, T4) TypeList<T1, TYPELIST_3(T2, T3, T4)>
#define TYPELIST_5(T1, T2, T3, T4, T5) TypeList<T1, TYPELIST_4(T2, T3, T4, T5)>
#define TYPELIST_6(T1, T2, T3, T4, T5, T6) TypeList<T1, TYPELIST_5(T2, T3, T4, T5, T6)>
..........................................................................................

Loki library defines macros up to 50 classes, but you can get an idea from these defines. In the Loki library there is a template MakeTypeList that automatically generates TypeList up to 18 data types.

To automate the registration of algorithms, we are using a template RegFactoryClasses (file TypeLists.h):

C++
template <class TList, int idx>
struct RegFactoryClasses;
template <class Head, class Tail>
struct RegFactoryClasses<TypeList<Head, Tail>, 0>
{
        typedef Head Result;
  static inline void Fn(void)
  {
    Result::RegisterWithFactory();
  }
};
template <class Head, class Tail, int idx>
struct RegFactoryClasses<TypeList<Head, Tail>, idx>
{
  typedef typename RegFactoryClasses<Tail, idx - 1>::Result Result;
  static inline void Fn(void)
  {
    Result::RegisterWithFactory();
    RegFactoryClasses<TypeList<Head, Tail>, idx - 1>::Fn();
  }
};

The compiler at compile time begins unwinding the template, each time calling the static function Head::RegisterWithFactory(), where Head is some class from the TypeList. The compiler stops at index zero.

So we need to define our TypeList for six algorithms now in the application:

C++
typedef TYPELIST_6(CASTravel, CASETravel, CRASTravel, CBWASTravel, CMMASTravel, CACSTravel) AlgTypeList;

Also we typedef our structure RegFactoryClasses:

C++
typedef RegFactoryClasses<AlgTypeList, TypeListLength<AlgTypeList>::value - 1 > RegAlgStruct;

Here TypeListLength is a meta-function also defined in Andrei's book:

C++
template <class TList> 
struct TypeListLength;
template <>
struct TypeListLength<NullType>
{
  enum {value = 0};
};
template < class T, class U >
struct TypeListLength<TypeList<T, U> >
{
  enum { value = 1 + TypeListLength<U>::value};
};

Now it is elementary: in OnInitDialog() of the main dialog, you call:

C++
RegAlgStruct::Fn();

It starts calling the algorithms' RegisterWithFactory functions beginning from the last type in the TypeList, CACSTravel. In the process of registration, the creation functions of the algorithms are being added to the map of the registered algorithms with algorithm names as keys. Because the map is a sorted container, the order of keys in the map is not immediately clear.

We are presenting algorithm types for the user selection in a combo box of the main dialog, and we want the box to show them in the same order they appear in the TypeList at the time of registration. Therefore, at the time of registration, we add the algorithm name to the vector of strings. The vector is a data member of the algorithm factory. Later, we initialize the combo box using this vector, not worrying about the number and names of algorithms. Because the first class in the TypeList is registered last, we start from the end of the vector:

C++
V_ALGNAMES::reverse_iterator rIt =  CTravelAlgFactory::m_vAlgNames.rbegin();
V_ALGNAMES::reverse_iterator rItE = CTravelAlgFactory::m_vAlgNames.rend();
for (int idx = 0; rIt != rItE; ++rIt, ++idx)
{
  m_comboAlg.InsertString(idx, rIt->c_str());
}

I have mentioned earlier that additional algorithm parameters are being set via dialogs. The dialogs are created on heap. To free the memory upon destruction of algorithm, we are using the TypeList again. It is the template (file TypeLists.h):

C++
template <class Head, class Tail, int idx>
struct DestroyParamsDlgs<TypeList<Head, Tail>, idx>

Statistics

Again, there is a hierarchy of statistics, all derived from a base class (file TStat.h):

C++
class CTStatBase
{
public:
  CTStatBase(void);
  virtual ~CTStatBase(void);
public:
  void UpdateDataCapacity(size_t maxRun);
  virtual int TravelStatID(void) const = 0;
  virtual V_CHARTNAMES* ChartNames(void) = 0;
  virtual void InitStatParams(void) {};
  virtual void CalculateDataSample(const CColony& antColony) = 0;
  virtual size_t ExportData(CChartContainer& chartContainer, int initEpoch) = 0;
public:
  V_STATDATA m_vData1;   // Data time series
  V_STATDATA m_vData2;
};

All statistics data are stored in two std::vector<double> data members, m_vData1 and m_vData2. What exactly is stored depends on the type of statistics. For example, for Best to date and Best per iteration statistics are length of travel for the best to date and best for iteration ants.

To optimize the use of memory, we use the function UpdateDataCapacity(size_t maxRun). It reserves the memory for the vector exactly to maxRum elements.

The key function here is CalculateDataSample. It is called upon completion of one trip.

The statistics data are presented to the user as numbers and charts. The number of charts per statistics varies. For example, the Distance statistics has three charts: an average distance, and average ± standard deviation charts. The names of the charts are stored in the vector of chart names that is a data member of the concrete statistics.

Upon the user or application's request, the function ExportData takes the accumulated chunk of data, calculates data for the charts, and appends charts. It happens every 100 trips, or on each stop, and on completion of the test.

The statistics classes are:

  • CTStatBest: gathers travel lengths for best to date and best for iteration ants
  • CTStatWorst: travel lengths for worst to date and worst for iteration ants
  • CTStatLength: average and standard deviation of lengths of ant trips for current iteration
  • CTStatDist: average and standard deviation for distances for current iteration (see Travel Statistics above)
  • CTStatBranch: the same for branching (see Travel Statistics)
  • CTStatCities: distribution of start cities
  • CTStatRst: intervals between consecutive resets, if the resets are enabled

This is an example of a concrete statistics class:

C++
class CTStatBest : public CTStatBase
{
public:
  CTStatBest(void);
  virtual ~CTStatBest(void);
public:
  static CTStatBase* CreateTStatBest() { return new CTStatBest;}
  virtual int TravelStatID() const {return m_statID;}
  virtual V_CHARTNAMES* ChartNames() {return &m_vChartNames;}
  virtual void CalculateDataSample(const CColony& antColony);
  virtual size_t ExportData(CChartContainer& chartContainer, 
                                                 int initEpoch);
  static bool RegisterWithFactory(void);
public:
  static const int m_statID;
  V_CHARTNAMES m_vChartNames;
};

You can see the implementation in the file TStat.cpp.

Again, we are using the class factory and TypeList to register the statistics with the factory:

C++
typedef TYPELIST_7(CTStatBest, CTStatWorst, CTStatLength, CTStatDist,
                 CTStatBranch, CTStatCities, CTStatRst) TStatList;
typedef RegFactoryClasses<TStatList, 
                 TypeListLength<TStatList>::value -1> RegTStatStruct;

As you can see, there are seven statistics, and RegFactoryClasses uses TStatList, not AlgTypeList.

The trick is, the statistics classes also have static functions named RegisterWithFactory, so there is no need to overwrite the definition of RegFactoryClasses.

Besides, we are using TypeList to initialize a static data member m_statID of a Statistics class. For example:

C++
const int CTStatBest::m_statID = IndexOf<TStatList, CTStatBest>::value;

IndexOf is the structure defined by Andrei Alexandrescu:

C++
template <typename T> 
struct IndexOf<NullType, T>
{
  enum { value = -1};
};
template <class Tail, typename T>
struct IndexOf<TypeList<T, Tail>, T>
{
  enum {value = 0};
};
template <class Head, class Tail, typename T>
struct IndexOf<TypeList<Head, Tail>, T>
{
private:
  enum {temp = IndexOf<Tail, T>::value};
public:
  enum {value = temp == -1 ? -1 : 1 +temp};
};

TypeList and IndexOf together replace enum values.

Lambda expressions, STL algorithms, and this application

In this application, we often use STL algorithms like std::transform, std::minmax_element, std::for_each, etc. Often we are applying these algorithms to containers that keep some structures. To process the data, the STL algorithms need some predicates or functions to be supplied because operations like less_than or greater_than are often not defined for these structures. C++ lambda expressions come to rescue here.

For example, a code snippet below calculates the maximum value of cities coordinates to get cities' extent, the square that envelops all cities (file AntColDlg.cpp):

C++
float CAntColDlg::GetCitiesExtent(const MAP_CITIES& mapCities)
{
  float maxCoord = 0.0f;
  for_each(mapCities.begin(), mapCities.end(), 
     [&maxCoord](const PAIR_CITY& pair_city) ->void
     {PointF pntF = pair_city.second.m_pntLocationF; 
      float locM = max(pntF.X, pntF.Y); 
      if (maxCoord < locM) maxCoord = locM;});
....................................................
}

It is an example of a lambda expression with the capture clause.

First, we define the range of iterators to operate on (it is the entire map of the cities).

Second, we define a local variable maxCoord = 0.0f.

Next, we call the algorithm for_each on iterators range. The lambda expression defines the calculations we are going to do.

The variables in the square brackets are captured variables. VS 2012 Help says: "The lambda expression can access any variable that has automatic storage duration and that can be accessed in the enclosing scope." It is a very powerful feature of lambda expressions.

If the lambda expression is going to change a value of the captured local variable, you have to pass it by reference.

Next is a parameter list. As it is usual for STL algorithms, it dereferences the container iterator.

->void is an expression's return type. That method of declaring the function return type is introduced in C++ 11 standard.

And finally, there is a body of the expression.

We access the member of the City, m_pntLocationF, select the maximum of its coordinates X or Y, and update the captured local variable maxCoord if needed.

In the code, you find the lambda expressions used with std::transform and other STL algorithms.

Bring your own ant colony optimization algorithm

You can see that including your own algorithm in this application is simple.

  • First, write your algorithm class derived from CASTravelBase. Append files TravelAlg.h and TravelAlg.cpp to do that. It will save you from headaches of finding and including the appropriate headers if you would use separate algorithm files.
  • If your algorithm has parameters, declare them as static data members of your class. Provide a dialog box or another means to set these parameters. Remember that you have to provide a pair of static date members, the value, and the value name string for each parameter to ensure saving/loading the parameters to/from XML files by the application.
  • Add your class type to the TypeList definition of AlgTypeList. Do not forget to change the number in the TypeList macro; for example, if you are adding a class, change TYPELIST_6 to TYPELIST_7, etc.
  • Compile, build, and run.

Bring your own statistics

It is similar, but more complicated. In addition to your own statistics class and the correction of the statistics type list you will have to add a button to the statistics selection dialog box, and your charts (if any) to the function CAntColDlg::ResetStChartData in AntColDlg.cpp.

Running an algorithm

Select an algorithm, its parameters, and the appropriate statistics. After selection, the algorithm and its parameters and the selected statistics are committed by clicking on the button Apply (function CAntColDlg::OnBnClickedBtnapply()). It enables the buttons Run or Step. Clicking on one of these buttons will start a worker thread. The thread receives the function:

C++
unsigned _stdcall CAntColDlg::RunASTravel(void* colDataPtr) 

with the pointer to CAntColDlg as a parameter:

C++
m_hWorkThread = (HANDLE)_beginthreadex(NULL, 0, CAntColDlg::RunASTravel, this, CREATE_SUSPENDED , NULL);

The execution of the algorithm is controlled by this function and the function:

C++
CAntColDlg::OnTravelOnce(WPARAM wParam, LPARAM lParam)

In C++ 11 there is a new class std::thread that accepts worker functions with many parameters. I did not use it because this class is included in VS 2012 only.

Here is the pseudo code for the worker function CAntColDlg::RunASTravel:

  • Prepare an array of event objects to synchronize the worker and the main threads
  • Get the max trials number and enter the loop to execute the trips
  • Reset the ant colony
  • Travel the colony graph for all colony's ants for the selected algorithm
  • Upon the end of each trip analyze the requests for graph snapshots and set the snapshot flag if the snapshot trigger is signaled
  • Calculate and save statistics for the trip
  • Check for reset and mutation conditions and do reset or mutation if needed
  • Update the ant colony graph pheromones
  • Check whether it is time to send data to the main thread for periodic display of the travel state (every 100 trips) or to allow a snapshot to be taken and set the appropriate control events
  • Enter the wait function and wait on events
  • Exit the wait function if appropriate and return to the beginning of the trip loop

Let us discuss the synchronization of the worker and main threads.

Synchronization of the worker and the main threads

We are using four Windows event objects to synchronize the worker and main threads.

The event handles are members of CAntColDlg and are created in CAntColDlg::OnInitDialog:

C++
m_hEvAbortRun = CreateEvent(NULL, FALSE, FALSE, NULL);// Auto reset,  // nonsignaled
m_hEvStopReq  = CreateEvent(NULL, FALSE, FALSE, NULL);// The same 
m_hEvInfoReq  = CreateEvent(NULL, FALSE, FALSE, NULL);// The same
m_hEvContRun  = CreateEvent(NULL, TRUE,  TRUE, NULL); // Manual Reset,    // signaled

The events are ordered at the start of the worker thread in two arrays:

C++
HANDLE syncEvArr[] = 
{ 
  antColDlgPtr->m_hEvAbortRun, antColDlgPtr->m_hEvStopReq, 
    antColDlgPtr->m_hEvInfoReq,  antColDlgPtr->m_hEvContRun 
};
HANDLE waitEvArr[] = 
{ 
  antColDlgPtr->m_hEvAbortRun, 
  antColDlgPtr-m_hEvContRun
};

The events are initialized:

C++
ResetEvent(syncEvArr[0]);
ResetEvent(syncEvArr[1]);
ResetEvent(syncEvArr[2]);
SetEvent(syncEvArr[3]);

The event to continue the run is last in both arrays. At the start, only the event to continue run is signaled.

After the worker thread finishes processing of the trip, it enters the wait function:

C++
DWORD dwWait = WaitForMultipleObjects(4, syncEvArr, FALSE, INFINITE);
switch (dwWait)
{
case WAIT_OBJECT_0:       // Terminated by user
  antColDlgPtr->PostMessage(UWM_ONCETRAVELED, MODE_FAIL, runCnt);
  return MODE_FAIL;;
case WAIT_OBJECT_0 + 1:   // Stop initiated
  if (antColDlgPtr->m_citiesMode == MODE_STEP)
    antColDlgPtr->PostMessage(UWM_ONCETRAVELED, MODE_STEP, runCnt);
    else
    antColDlgPtr->PostMessage(UWM_ONCETRAVELED, MODE_STOP, runCnt);
  dwWait = WaitForMultipleObjects(2, waitEvArr, FALSE, INFINITE);
  switch (dwWait)
  {
  case WAIT_OBJECT_0:     // Terminated by user
    antColDlgPtr->PostMessage(UWM_ONCETRAVELED, MODE_FAIL, runCnt);
    return MODE_FAIL;
  case WAIT_OBJECT_0 + 1: // Continue; mode is to set by caller
    break;
  }
  break;
case WAIT_OBJECT_0 + 2:   // Interrupted to display the state; needs to
                                                    // set cont. event
  antColDlgPtr->PostMessage(UWM_ONCETRAVELED, MODE_RUN, runCnt);
  dwWait = WaitForMultipleObjects(2, waitEvArr, FALSE, INFINITE);
  switch (dwWait)
  {
  case WAIT_OBJECT_0:     // Terminated by user
    antColDlgPtr->PostMessage(UWM_ONCETRAVELED, MODE_FAIL, runCnt);
    return MODE_FAIL;
  case WAIT_OBJECT_0 + 1: // Continue; mode is to be set by caller
    break;
  }
  break;
case WAIT_OBJECT_0 + 3:   // Normal continuation
  break;
}

If the main thread (the user) wants to stop or interrupt test execution, it should reset the continue event and set one of the other events, like this:

C++
if (m_citiesMode == MODE_RUN)
{
  ResetEvent(m_hEvContRun);
  SetEvent(m_hEvInfoReq);
}

The first action is to reset m_hEvContRun. Because all other events are normally non-signaled, the worker thread will go into the wait state. It waits until the user will set any other event to the signaled state. That will release the worker thread, and the worker function will go to the switch statement to process the return from the first wait function.

First, the worker thread posts a message to the main thread:  

C++
antColDlgPtr->PostMessage(UWM_ONCETRAVELED, MODE_RUN, runCnt);

The worker thread passes the appropriate values to WPARAM and LPARAM of the message.

If the user requested an interruption of the test (had set event m_hEvStopReq or m_hEvInfoReq), to initiate one step or stop of execution, or the application requested the information on the state of the travel, the worker thread enters the second wait functions, to wait for the user choice to continue or to abort the run. This time the worker thread is waiting for two events only.

The messages the worker thread sends to the main thread are handled in the function CAntColDlg::OnTravelOnce(WPARAM wParam, LPARAM lParam) (file AntColDlg.cpp).

This function, after completing all processing, sets the event m_hEvContRun if the user wants to continue the run. That releases the worker thread if it was waiting.

After completing the run, the worker thread sends to the main thread the message:

C++
antColDlgPtr->PostMessage(UWM_ONCETRAVELED, MODE_APPLY, runCnt);

and exits.

Drawing with GDI+

All drawing is rendered in GDI+. To get flicker free rendering, I am drawing onto a memory bitmap and only at the end of the drawing transfer the bitmap onto the screen surface (this is well known as double buffering). Here is an example:

C++
void CStCities::OnPaint()
{
  CPaintDC dc(this);                 // Device context for painting
  Graphics gr(dc.m_hDC);             // Graphics to paint
  Rect rGdi;
  gr.GetVisibleClipBounds(&rGdi);   // The same as the clip rect
  rGdi.Width -= 1;                  // Important: right and bottom
  rGdi.Height -= 1;                 // borders are outside the rectangle          
  Bitmap clBmp(rGdi.Width, rGdi.Height);          // Memory bitmap
  Graphics* grPtr = Graphics::FromImage(&clBmp);  // As memDC
  .................................................
        // Drawing routines
  .................................................
 
  gr.DrawImage(&clBmp, rGdi);
  delete grPtr;
}

Second, to display the start cities as a bar graph, I used a transparent picture control CCitiesChart.

I overlapped it over the main dialog chart control. The code is in CAntColDlg::OnInitDialog():

C++
WINDOWPLACEMENT contPlacement;
m_chartContainer.GetWindowPlacement(&contPlacement);
m_stCitiesChart.SetWindowPlacement(&contPlacement);

Printing outside a document/view model

In the article KB133275, Microsoft explains how to print from a class other than an MFC CView. In addition, there is a tutorial on GDI Printing, GDI+ Printing on Internet. The tutorial is overcomplicated; Microsoft does not mention GDI+.

Still, I followed the framework of the Microsoft sample code.

For example, this is how I am printing a report of the results of the test.

First, you start with CPrintDialog to select the printer and get the printer DC (AntColDlg.cpp):

C++
void CAntColDlg::PrintReport(void)
{
  CPrintDialog printDlg(FALSE,PD_USEDEVMODECOPIES|PD_HIDEPRINTTOFILE|PD_NOPAGENUMS|
                   PD_NOSELECTION|PD_RETURNDC);
  printDlg.m_pd.Flags &= ~PD_SHOWHELP;
  printDlg.m_pd.Flags &= ~PD_NOSELECTION;
  if (IDOK == printDlg.DoModal())
  {
// Get screen dpi to scale all literals like pen width, offsets, etc. e  
    CPaintDC containerDC(this);
    int scrDpiX = containerDC.GetDeviceCaps(LOGPIXELSX);
// Init and run prn report
    CPrnReport prnReport;     // Instantiate the print report class     
    prnReport.InitRepData(this);
// If true, print the first page only
    bool bPrintSel = printDlg.PrintSelection() == TRUE ? true : false;
    prnReport.PrintReport(scrDpiX, bPrintSel, printDlg.GetPrinterDC());
  }
}

All work is done in the function (file PrnReport.cpp):

C++
void CPrnReport::PrintReport(int scrDpiX, bool bPrintSel, HDC printDC)
{
  if (printDC != NULL)
  {
    CDC* pDC = new CDC;
    pDC->Attach(printDC); 
    Graphics* grPtr = Graphics::FromHDC(printDC);  // Graphics to paint
    grPtr->SetPageUnit(UnitDocument);
    grPtr->SetSmoothingMode(SmoothingModeAntiAlias);
// Get dpiRatio
    float dpiRatioX = 300.0f/scrDpiX;
    RectF rGdiF;
        grPtr->GetVisibleClipBounds(&rGdiF);     // The same as the clip rect
// Begin Printing
    pDC->StartDoc(m_algName.c_str());       // MFC functions
    pDC->StartPage();
..........................................  // Drawing functions 
    pDC->EndPage();
// Now print the best to date ant's path as a table
    if (!bPrintSel)
    {
      ...........................................
      while (true)
      {
        pDC->StartPage();
        ................................. // Drawing functions 
        pDC->EndPage();
        if (firstRow == rowsTotal)
          break;
      }
    }
    pDC->EndDoc();
    delete(pDC);
  }
}

Pay attention to the use of MFC functions StartDoc, StartPage, EndPage, and EndDoc.

Creating a custom print dialog

The recipe is known: find the CPrintDialog resource template in MFC, copy it, extend it, and write your own class derived from CPrintDialog with all the bells and whistles you like.

I spent a lot of time trying to find the appropriate resource, and I have found it eventually. You can copy IDD_DLGEDGEPRINT from this application to save some time.

You have to initialize the members of the m_pd structure of CFileDialog like this (file DlgEdgePrint.cpp):

C++
CDlgEdgePrint::
  CDlgEdgePrint(BOOL bPrintSetupOnly, DWORD dwFlags, CWnd* pParentWnd)
               : CPrintDialog(bPrintSetupOnly, dwFlags, pParentWnd)
{
  m_pd.lpPrintTemplateName = (LPTSTR) MAKEINTRESOURCE(IDD_DLGEDGEPRINT); 
      m_pd.Flags |= PD_ENABLEPRINTTEMPLATE;  
  m_pd.Flags &= ~PD_SHOWHELP;
      m_pd.hInstance = AfxGetInstanceHandle(); 
}

You also have to write handlers for controls you added to the dialog template.

In my custom print dialog, I have added only a CStatic control to display a warning that was known before the print dialog was instantiated:

C++
sstream_t stream_t;
stream_t << _T("Selected screen page takes ") << paperPagesPerScrPage 
   << _T(" paper pages,\n") << _T("All pages need ") 
         << totalPaperPages << _T(" paper pages.\n") 
   << _T("Screen page #1 shows ant's path,\nfor other pages") 
   << _T(" source vertex's ID = page nmb -2") << endl;
string_t str = stream_t.str();
const _TCHAR* buf = str.data();

I have initialized the m_pd members as follows:

C++
CDlgEdgePrint printDlg(FALSE, PD_SELECTION|PD_USEDEVMODECOPIES|PD_HIDEPRINTTOFILE||PD_RETURNDC);
printDlg.m_pd.Flags    &= ~PD_SHOWHELP;
printDlg.m_pd.Flags    |= PD_ENABLEPRINTHOOK;
printDlg.m_pd.lpfnPrintHook = PrintHookProc;
printDlg.m_pd.nFromPage = WORD(selPage + 2);
printDlg.m_pd.nToPage   = WORD(selPage + 2);
printDlg.m_pd.nMinPage  = 1; 
printDlg.m_pd.nMaxPage  = WORD(pagesNum);
printDlg.m_pd.lCustData = (LPARAM)buf;

I wrote this text in the static control on initialization of the dialog calling the callback function PrintHookProc:

C++
UINT_PTR CALLBACK PrintHookProc(HWND hdlg, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
  UNREFERENCED_PARAMETER(wParam);
  switch(uiMsg)
  {
  case WM_INITDIALOG:
    {
      PRINTDLG* pd = (PRINTDLG*)lParam;
      const _TCHAR* buf = (_TCHAR*)pd->lCustData;
      HWND tbHWND = (HWND)GetDlgItem(hdlg, IDC_EDGEPRN_STWARN);
      SetWindowText(tbHWND, buf);
    }    break;
  }
  return FALSE;
}

The source code and demo project

With this article, I have included the application source code and the AntCol.exe file.

With the source code are included the libraries it uses: ChartCtrlLibD.lib (Debug version 1.2.1) and ChartCtrlLib.lib (Release version 1.2.1.) You will be able to recompile and rebuild AntCol.exe with your own algorithms.

The code was built under MS Visual Studio 2012 (VC++ 11) and Windows & Pro.

If you do not have VS 2012, you still can work with the source code in MS VS 2010. I included the solution and project files in the folder 2010Project in the source zip file.  The source files are the same, the only difference is that for VC++ 11, the project properties include a preprocessor directive _VARIADIC_MAX=10, because in VC++ 11 Microsoft uses variadic templates for templates with many arguments, and default max number of elements is five. In VC++ 10, they are using recurrent definitions like we used for TypeList.

To work in VS 2010, you will need VC++ 10 versions of the static library ChartCtrlLib.lib. You can download them from my article: An MFC Chart Control with Enhanced User Interface.

History

  • 08/22/2013: Initial version, 1.0.0.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)