Get the full list of Windows supported countries with C#

When developing the LocaliZune settings editor, one of the challenge was to display a full list of Windows countries to the user. My first approach to meet this challenge was to retrieve a list of countries using the CultureInfo.GetCultures() method and then get all the country information following this example. But I quickly realised that a lot of countries were missing from the list and that it does not matched the country list from control panel location setting. I investigated more deeply on how I could provide the full list of countries supported by the system.

These investigations finally led me to the ultimate solution: invoking EnumSystemGeoID and GetGeoInfo from kernel32 system library.

The only way to get a full list of countries is actually to do some P/Invoke because .NET 4.0 does not provides this capability. So I ended up writing my own wrapper to invoke native methods and finally get a full list of countries supported by the current Windows OS.

public sealed class SystemGeographicalLocation
{
	public const int GEOCLASS_NATION = 0x10;

	private SystemGeographicalLocation()
	{
	}

	public delegate bool EnumGeoInfoProc(int GeoId);

	public enum SYSGEOTYPE
	{
		GEO_NATION = 0x0001, of the location.
		GEO_LATITUDE = 0x0002,
		GEO_LONGITUDE = 0x0003,
		GEO_ISO2 = 0x0004,
		GEO_ISO3 = 0x0005,
		GEO_RFC1766 = 0x0006,
		GEO_LCID = 0x0007,
		GEO_FRIENDLYNAME = 0x0008,
		GEO_OFFICIALNAME = 0x0009,
		GEO_TIMEZONES = 0x000A,
		GEO_OFFICIALLANGUAGES = 0x000B
	}

	[DllImport("Kernel32.dll", SetLastError = true)]
	public static extern int GetGeoInfo(int Location, SYSGEOTYPE GeoType, StringBuilder lpGeoData, int cchData, int LangId);

	[DllImport("Kernel32.dll", SetLastError = true)]
	public static extern int EnumSystemGeoID(int GeoClass, int ParentGeoId, EnumGeoInfoProc lpGeoEnumProc);
}

Then to contain the data I declared a simple type that would match the SYSGEOTYPE enumeration.

public class GeographicalLocation
{
	public string Nation { get; set; }
	public string Latitude { get; set; }
	public string Longitude { get; set; }
	public string ISO2 { get; set; }
	public string ISO3 { get; set; }
	public string Rfc1766 { get; set; }
	public string Lcid { get; set; }
	public string FriendlyName { get; set; }
	public string OfficialName { get; set; }
	public string TimeZones { get; set; }
	public string OfficialLanguages { get; set; }
}

And finally, to make it easy to use, I developed a helper class that would do the platform invocation for me and return a list of GeographicalLocation.

public static class GeographicalLocationHelper
{
	private static List<GeographicalLocation> geographicalLocations;
	private static List<int> geoIds;
	private static SystemGeographicalLocation.EnumGeoInfoProc callback;
	private static int lcid;

	static GeographicalLocationHelper()
	{
		geographicalLocations = new List<GeographicalLocation>();
		geoIds = new List<int>();
		callback = EnumGeoInfoCallback;
		lcid = CultureInfo.CurrentCulture.LCID;
	}

	public static IEnumerable<GeographicalLocation> GetGeographicalLocations()
	{
		if (geographicalLocations.Count == 0)
		{
			SystemGeographicalLocation.EnumSystemGeoID(SystemGeographicalLocation.GEOCLASS_NATION, 0, callback);

			foreach (var geoId in geoIds)
			{
				GeographicalLocation location = new GeographicalLocation();

				location.Nation             = GetGeoInfo(geoId, SystemGeographicalLocation.SYSGEOTYPE.GEO_NATION, lcid);
				location.Latitude           = GetGeoInfo(geoId, SystemGeographicalLocation.SYSGEOTYPE.GEO_LATITUDE, lcid);
				location.Longitude          = GetGeoInfo(geoId, SystemGeographicalLocation.SYSGEOTYPE.GEO_LONGITUDE, lcid);
				location.ISO2               = GetGeoInfo(geoId, SystemGeographicalLocation.SYSGEOTYPE.GEO_ISO2, lcid);
				location.ISO3               = GetGeoInfo(geoId, SystemGeographicalLocation.SYSGEOTYPE.GEO_ISO3, lcid);
				location.Rfc1766            = GetGeoInfo(geoId, SystemGeographicalLocation.SYSGEOTYPE.GEO_RFC1766, lcid);
				location.Lcid               = GetGeoInfo(geoId, SystemGeographicalLocation.SYSGEOTYPE.GEO_LCID, lcid);
				location.FriendlyName       = GetGeoInfo(geoId, SystemGeographicalLocation.SYSGEOTYPE.GEO_FRIENDLYNAME, lcid);
				location.OfficialName       = GetGeoInfo(geoId, SystemGeographicalLocation.SYSGEOTYPE.GEO_OFFICIALNAME, lcid);
				location.TimeZones          = GetGeoInfo(geoId, SystemGeographicalLocation.SYSGEOTYPE.GEO_TIMEZONES, lcid);
				location.OfficialLanguages  = GetGeoInfo(geoId, SystemGeographicalLocation.SYSGEOTYPE.GEO_OFFICIALLANGUAGES, lcid);

				geographicalLocations.Add(location);
			}
		}

		return geographicalLocations;
	}

	private static string GetGeoInfo(int location, SystemGeographicalLocation.SYSGEOTYPE geoType, int langId)
	{
		StringBuilder geoData = new StringBuilder();
		int bufferSize = 0;

		bufferSize = SystemGeographicalLocation.GetGeoInfo(location, geoType, geoData, 0, langId);

		if (bufferSize > 0)
		{
			geoData.Capacity = bufferSize;
			SystemGeographicalLocation.GetGeoInfo(location, geoType, geoData, bufferSize, langId);
		}

		return geoData.ToString();
	}

	private static bool EnumGeoInfoCallback(int geoId)
	{
		if (geoId != 0)
		{
			geoIds.Add(geoId);

			return true;
		}

		return false;
	}
}

Now you will have a full list of countries available on the current system with their nation code and their RFC1766 code which you can use to instantiate the corresponding RegionInfo object. For some reasons, all the returned countries have the same LCID, therefore you cannot really use it for anything.

You can download a demo solution which display the full list of countries in a combo box and includes code documentation and commenting: GeographicalLocation.zip

This entry was posted in C#, P/Invoke and tagged , , , , . Bookmark the permalink. Post a comment or leave a trackback: Trackback URL.

3 Comments

  1. Chris
    Posted April 4, 2012 at 19:22 | Permalink

    Thanks, I had something similar I found from some OLD VB6 code but this is a little better then what I had as more information is put into the collection.

    • Lionel
      Posted April 4, 2012 at 20:06 | Permalink

      I also went through this VB code but when I realised that the list was incomplete I decided to investigate a bit further and found this function in the Win32 API that gave the full list used by the system.

  2. Oleg
    Posted April 10, 2012 at 09:34 | Permalink

    Thank’s man!
    I have already discovered EnumSystemGeoID and prepared to do some p/invoke exercises ;)

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*

You may use these HTML tags and attributes <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>