Tuesday, October 11, 2011

Google Maps in a desktop app

I’ve seen one or two examples of using Google Maps in a WinForms desktop app, but the ones I’ve seen seem to involve loading image tiles from the Google server directly. There’s nothing wrong with that approach but I thought it would be a lot simpler to simply host a local web page using the Google Maps API in a WebBrowser control in an application. Here is a very simple example of this idea.

If you want to extend this example, it’s possible to call scripts in the page via the WebBrowser.Document.InvokeScript method and the application can respond to events in the page via the WebBrowser.ObjectForScripting property.

As an aside, similar ideas can be applied to hosting other Javascript web components, such as an HTML editor like CKEditor.

Thursday, July 14, 2011

Google Maps and Friend Connect weirdness in IE8

I received a bug report about my website. The maps that appear on my UK postcodes pages weren’t working in IE8. I hadn’t noticed since they were working fine in IE9 but when I switched to compatibility view in IE9 I started to see the same problem. This helped because I was then able to debug my JavaScript (much as I love IETester, I’d love it even more if I had access to the developer tools for each version of IE). And debugging the script revealed google.maps was null.

At this point I assumed this was a problem with my Google Maps script, so tried loading Google Maps asynchronously then tried specifying an older version of the API. Neither helped. I tried inserting the script in my HTML head but with no luck.

Finally I had a look at the google object and saw the only thing defined in there was friendconnect. Now my suspicions moved away from Google Maps to Google Friend Connect. my Friend Connect stuff appears at the bottom of each page and the script reference for it was also down the bottom of the page. So I thought I’d tried moving the Friend Connect script reference to the html header. And voila, the maps started working again.

So the conclusion? I’m not really sure, although I suspect Friend Connect is removing the google.maps object, although it seems odd that it only happens on IE8.

Saturday, June 04, 2011

Retrieving the most popular pages using Google Analytics API

For a long time I’ve shown the most popular pages on the home page of my website. I did this by logging every page that was viewed to the MySql database on the back end. This kind of worked but had a few problems. First, it wasn’t very clever since it couldn’t tell the difference between a real visitor and a search engine bot. Second, since I’ve started to get quite a few visitors (no, really), it was writing a large amount of data to the database.

So I thought there must be a better solution. Figuring that all the information I needed was already being collected by Google Analytics, I thought I could grab this data and dump it into a much smaller with just the page URL and the number of visits (rather than adding a row for every visit). So I coded up a solution using the .NET wrapper around the Google Analytics API. And this is what it looks like (with the database access code removed for clarity). You’ll need to provide your own email address, password and Google Analytics account table ID to get this to work, for obvious reasons.

using System;
using Google.GData.Analytics;

namespace GoogleAnalytics
{
  class Program
  {
    static void Main(string[] args)
    {
      AccountQuery feedQuery = new AccountQuery();
      AnalyticsService service = new AnalyticsService("DoogalAnalytics");
      service.setUserCredentials("email address", "password");

      DataQuery pageViewQuery = new DataQuery("https://www.google.com/analytics/feeds/data");
      pageViewQuery.Ids = "Google Analytics account table ID";
      pageViewQuery.Metrics = "ga:visits";
      pageViewQuery.Dimensions = "ga:pagePath";
      pageViewQuery.Sort = "-ga:visits";
      pageViewQuery.GAStartDate = DateTime.Now.AddMonths(-1).ToString("yyyy-MM-dd");
      pageViewQuery.GAEndDate = DateTime.Now.ToString("yyyy-MM-dd");

      DataFeed feed = service.Query(pageViewQuery);
      for (int i = 0; i < 20; i++)
      {
        DataEntry pvEntry = (DataEntry)feed.Entries[i];
        string page = pvEntry.Dimensions[0].Value.Substring(1);
        string visits = pvEntry.Metrics[0].Value;

        Console.WriteLine(page + ": " + visits);
      }

      Console.ReadLine();
    }
  }
}

Monday, May 30, 2011

Poor man’s XSLT profiling for .NET

If you’ve ever looked round for a profiler for XSL transformations then chances are you’ve found the Microsoft add-on for Visual Studio, which looks like it’s just the ticket, if you happen to have Visual Studio Team System. But if you don’t happen to own that version, then it might look like you have to upgrade your VS license or buy some other XSLT profiler.

But if you happen to own a .NET profiler (I highly recommend AQTime) then there may be another solution. Visual Studio comes with the XSLTC tool that can be used to generate an assembly from an XSL transformation. Once we’ve got an assembly, then we can build a small wrapper application that loads up the assembly, passes it to an instance of the XslCompiledTransform class and calls the transform. And once we’ve got that, we can use a standard .NET profiler to find bottlenecks.

And as I understand it, the XSLT profiler add-on for Visual Studio works in just this way so profiling using this technique should be just as effective as the Microsoft version.

Thursday, April 14, 2011

Spotify not too good to be true anymore

Apparently I’ve been using Spotfiy for over two years. Funny, it seems longer than that. It was the perfect music service for me, unlimited music of my choosing on my PC, which is where I listen to music most of the time, with the only minor downside being some adverts that play occasionally between tracks. But it looks like it won’t be quite so perfect anymore. Free users can only listen to a track a total of five times and total listening time will be limited to 10 hours a month.

As a frequent user of Spotify I can see why they are doing this. First, it’s obvious that advertising revenue is not what they were hoping for, most of the ads are still for Spotify itself. Second, using it has had a perhaps not unexpected effect on my music buying behaviour. First example, U2 put their last album up on Spotify before its official release. I had a listen and realised it was rubbish, so as a marketing exercise I doubt it was a huge success. Second example/s, quite a few new releases are put onto Spotify Premium upon release. On a couple of occasions I’ve then purchased the album before it’s become available on the free version (Elbow, Arcade Fire if you’re wondering). I’m guessing this isn’t what Spotify wanted me to do, they were presumably hoping I’d pony up the Premium version. And then when those albums did become available on the free version (generally only a few weeks after release) I was hit with a mild feeling of regret for spending money that I didn’t really need to and deciding to think twice before making another purchase. Again, probably not what the music industry sponsors of Spotify were hoping for.

So now I’ve got a choice, sign up for a tenner a month and continue on as I am at the moment or spend that tenner on a CD every month. I guess the music industry don’t care too much which way that ten quid gets to them, so it’s purely a personal dilemma. But the people who can’t or won’t spend a tenner (teenagers, students mostly I guess) will probably rediscover the skill of searching for pirated albums on Google. The music industry is still caught between a rock and a hard place.  

Saturday, March 12, 2011

Updating the Code-Point postcode datataset in MySql

Some time ago I imported the Ordnance Survey Code-Point postcode dataset into MySql. It looks like there’s a new version of that dataset available which includes new postcodes so I wanted to update my database. I guess I could just empty the table and re-import the data, but since it takes some time import and the data is live on the web, this wasn’t the ideal solution. Fortunately, MySql has a useful IGNORE keyword which will ignore failed inserts so any old postcodes will be ignored (since the postcode is used as the primary key on the table) whilst new ones are inserted. Of course, this assumes that the latitude and longitude of old postcodes doesn’t change, which I’m hoping is a reasonable assumption. So my new code looks like this.

using System;
using System.IO;
using DotNetCoords;
using LumenWorks.Framework.IO.Csv;
using MySql.Data.MySqlClient;

namespace ImportCodepoint
{
  class Program
  {
    static void Main(string[] args)
    {
      string[] files = Directory.GetFiles(@"C:\Users\Doogal\Downloads\codepo_gb\Code-Point Open\Data");
      foreach (string file in files)
      {
        ReadFile(file);
      }

    }

    private static void ReadFile(string file)
    {
      using (StreamReader reader = new StreamReader(file))
      {
        CsvReader csvReader = new CsvReader(reader, false);
        using (MySqlConnection conn = new MySqlConnection(
          "server=server;uid=username;pwd=password;database=database;"))
        {
          conn.Open();
          foreach (string[] data in csvReader)
          {
            string postcode = data[0];
            // some postcodes have spaces, some don't
            if (postcode.IndexOf(' ') < 0)
              postcode = data[0].Substring(0, data[0].Length - 3) + " " + data[0].Substring(data[0].Length - 3);
            // some have two spaces...
            postcode = postcode.Replace("  ", " ");
            
            double easting = double.Parse(data[10]);
            double northing = double.Parse(data[11]);

            // there are some postcodes with no location
            if ((easting != 0) && (northing != 0))
            {
              // convert easting/northing to lat/long
              OSRef osRef = new OSRef(easting, northing);
              LatLng latLng = osRef.ToLatLng();
              latLng.ToWGS84();

              using (MySqlCommand command = conn.CreateCommand())
              {
                Console.WriteLine(postcode);
                command.CommandTimeout = 60;
                command.CommandText = string.Format(
                  "INSERT IGNORE INTO Postcodes (Postcode, Latitude, Longitude) " +
                  "VALUES ('{0}', {1}, {2})",
                  postcode, latLng.Latitude, latLng.Longitude);
                int count = command.ExecuteNonQuery();
                if (count > 0)
                  Console.WriteLine("Added");
              }
            }
          }
        }
      }
    }
  }
}

Tuesday, February 22, 2011

GZipping all content served up by ASP.NET

Update – I now realise this post is kind of pointless, there is a module for compression of dynamic content, called unsurprisingly DynamicCompressionModule… But the approach described may be useful for someone somewhere…

I couldn’t find anything that will GZip all the content returned by ASP.NET. There’s a module for compression of static files but nothing for dynamic content. There may be a good reason for this, perhaps the overhead of GZipping content on the fly can kill your server, but since my current project has no static content I thought it would be useful to give it a go. The solution is pretty simple, register the following module in web.config and you’re good to go.

using System;
using System.IO.Compression;
using System.Web;

namespace MyNamespace
{
  public class GzipModule : IHttpModule
  {
    public void Dispose()
    {
      
    }

    public void Init(HttpApplication context)
    {
      context.BeginRequest += new EventHandler(context_BeginRequest);
    }

    void context_BeginRequest(object sender, EventArgs e)
    {
      HttpApplication app = (HttpApplication)sender;
      if ((app.Request.Headers["Accept-Encoding"] != null) &&
            (app.Request.Headers["Accept-Encoding"].Contains("gzip")))
      {
        app.Response.Filter = new GZipStream(app.Response.Filter, CompressionMode.Compress);
        app.Response.AppendHeader("Content-encoding", "gzip");
        app.Response.Cache.VaryByHeaders["Accept-encoding"] = true;
      }
    }
  }
}
Registration is as follows
    <modules>
            <add name="GzipModule" type="MyNamespace.GzipModule" />
    </modules>

Saturday, February 05, 2011

Fixing 404 errors when using ASP.NET 4 routing

It took me a while to figure this out. Routing is meant to be baked into ASP.NET 4 but when i tried to set it up, all I got was 404 errors. I did a lot of Googling but couldn’t find anything. It turned out all I was missing was this in web.config

<system.webServer>
    <modules runAllManagedModulesForAllRequests="true"></modules>

Thursday, February 03, 2011

Northern Ireland postcode data

The OS Code-Point Open dataset is great, except for a few omissions. It doesn’t include data for Northern Ireland, the Isle of Man or the Channel Islands. It turns out that the Northern Irish postcode data can be found here. Unfortunately that data is in ESRI and MapInfo formats, which I’m not sure how to read. Fortunately Jamie Thompson has converted it to CSV, which is a little easier to deal with.

From that CSV file, it’s quite simple to import the data into SQL Server or MySql using code slightly modified from my Code-Point examples (SQL Server here and MySql here). The only thing to note is that the CSV file uses Irish grid references rather than OS grid references.

Now to figure out where to get hold of the Isle of Man and Channel Islands data…

Monday, January 31, 2011

Don’t believe everything that Reflector tells you

Every .NET developer loves Reflector, since it gives us a chance to see inside assemblies that we don’t have the source for. And I’ve even read bloggers showing off the code that has been reverse engineered by it as evidence of poor coding practices at some organisation or another (“look, these guys use gotos!”). But though Reflector is a brilliant tool, its reverse engineering skills are not perfect. See this fairly innocent looking switch statement from some code I’m working on 

        switch(type)
        {
          case "gateway":
            SetValue(component, "@type", "decision", true);
            string xml = component.InnerXml;
            xml = xml.Replace("gateway", "decision");
            component.InnerXml = xml;
            break;

          case "deliverable":
          case "dataObject":
            SetValue(component, "@type", "document", true);
            break;

          case "annotation":
            SetValue(component, "@type", "note", true);
            break;
        } 

And this is what Reflector shows from the compiled assembly

        if (CS$4$0001 != null)
        {
            if (!(CS$4$0001 == "gateway"))
            {
                if ((CS$4$0001 == "deliverable") || (CS$4$0001 == "dataObject"))
                {
                    goto Label_00B0;
                }
                if (CS$4$0001 == "annotation")
                {
                    goto Label_00C5;
                }
            }
            else
            {
                this.SetValue(component, "@type", "decision", true);
                string xml = component.InnerXml.Replace("gateway", "decision");
                component.InnerXml = xml;
            }
        }
        goto Label_00DA;
    Label_00B0:
        this.SetValue(component, "@type", "document", true);
        goto Label_00DA;
    Label_00C5:
        this.SetValue(component, "@type", "note", true);
    Label_00DA:;

Which I think proves my point…

Thursday, January 13, 2011

Better debugging of .NET services

For a long while I’ve been debugging a .NET service using the recommended approach. Whilst this works, it’s kind of painful. the steps are something like this

  1. Build the service
  2. Realise the service was already running. Stop it from the Services Control Panel applet.
  3. Build the service again
  4. Start the service from the Services Control Panel applet.
  5. Attach to the process from Visual Studio
  6. Realise the service has already executed the piece of code I wanted to debug.
  7. Goto step 1.

There had to be a better way. And a bit of Googling brought up this approach. But I didn’t really understand how it worked. I guess I’d assumed the error shown in Visual Studio when you try to debug a service was actually coming from Visual Studio, but I now realise the error is coming from .NET. So by having a different piece of code run when in debug mode, the service is treated like any old application.

My solution is slightly different so I can also test the service starting and stopping. My service implementation has StartService and StopService public methods, which are called from OnStart and OnStop. And my Main method looks like this.

    static void Main()
    {
      #if (!DEBUG)
      ServiceBase[] ServicesToRun;
      ServicesToRun = new ServiceBase[] 
            { 
                new MyService() 
            };
      ServiceBase.Run(ServicesToRun);
      #else
      WorkflowService service = new MyService();
      service.StartService();
      service.DoStuff();
      service.StopService();
      #endif               
    }

Friday, December 10, 2010

Onshoring, the new offshoring

Us software developers have had to live with offshoring for years. And I can understand why companies do it. If software isn’t your raison d'être then why not outsource it, and if you’re going to outsource it, why not send it offshore where you can get probably the work done cheaper?

But there are problems with offshoring. There are the time differences and cultural differences that may make it not go as smoothly as hoped. But I’m not one to think there’s any reason why software developed in another country should be any worse than software produced at home. I’ve dealt with many developers, at home and overseas and there are good and bad developers everywhere.

But now things have changed again. The UK government introduced a scheme, the intra company transfer visa, which is intended to bring in people to the UK to fill skills gaps that can’t be filled by the UK population. But big business have found some sweet loopholes in this scheme that means they can pay pretty much minimum wage to their developers whilst claiming back their expenses and not paying NI for them. I can understand why this is attractive to companies, it saves them money after all, and why should they care about the greater good? I doubt Henry Ford would survive very long in the modern world. But I can’t see it’s particularly good for the country as a whole, with lower tax revenue and higher unemployment being the obvious outcome. So what the hell was the government thinking of? I’m quite happy to compete with anybody on a level playing field, but when the playing field has been slanted like this, I’m pretty baffled about what’s going on.

Yesterday we had students smashing up the streets because they think it’s unfair that they are going to be in massive debt after they complete their degrees. In the past, graduates would be the people going into these relatively low paid jobs. How are they going to react when they get out of university to find there are no jobs available for them, due to this wonderful scheme?

Monday, November 22, 2010

Adding a simple website hit counter using the Google Analytics API

Last time, I built a simple app to get the number of visitors to a site using the Google Analytics API. So the obvious next step is to get that data displayed on a web page. The simplest way to do this would be to add some server-side code to an ASP.NET page. But if you’ve run the code in the previous blog entry, you’ll notice it’s pretty slow, generally taking over a second to get the data back from Google. Adding a second to every page load isn’t such a good idea, so this is my slightly more complicated solution, which loads up the hit count via AJAX.

First we need a generic handler, looking something like this

<%@ WebHandler Language="C#" Class="visitorCounter" %>

using System;
using System.Web;
using Google.GData.Analytics;

public class visitorCounter : IHttpHandler 
{
  public void ProcessRequest (HttpContext context) 
  {
    AccountQuery feedQuery = new AccountQuery();
    AnalyticsService service = new AnalyticsService("DoogalAnalytics");
    service.setUserCredentials("email", "password");
    DataQuery pageViewQuery = new DataQuery("https://www.google.com/analytics/feeds/data");
    pageViewQuery.Ids = "ga:103173";
    pageViewQuery.Metrics = "ga:visits";
    pageViewQuery.GAStartDate = DateTime.Now.AddMonths(-1).ToString("yyyy-MM-dd");
    pageViewQuery.GAEndDate = DateTime.Now.ToString("yyyy-MM-dd");
    DataEntry pvEntry = service.Query(pageViewQuery).Entries[0] as DataEntry;

    context.Response.ContentType = "text/plain";
    context.Response.Write(pvEntry.Metrics[0].Value);
  }
 
  public bool IsReusable {
    get {
       return false;
    }
  }
}

Then we just need a bit of jQuery magic to show it on the page.

    <p>
      Visitors this month: <span id="visitorCount"></span>
      <script type="text/javascript">
        $(document).ready(function () {
          $.get('visitorCounter.ashx', function (data) {
            $('#visitorCount').html(data);
          });
        });

      </script>
    </p>

With a little work this could probably be wrapped up as an ASP.NET control, but I’ll leave that as an exercise for the reader.

Monday, November 15, 2010

Simple Google Analytics .NET API usage

So if you want to create your own version of Embedded analytics, the first thing to do is figure out how to use the Google Analytics API. If you’re wanting to use the .NET API, then this little snippet might help you get started. It loops through all the registered sites and shows the number of visits to each site over the last month.

  class Program
  {
    static void Main(string[] args)
    {
      AccountQuery feedQuery = new AccountQuery();
      AnalyticsService service = new AnalyticsService("DoogalAnalytics");
      service.setUserCredentials("email", "password");
      foreach (AccountEntry entry in service.Query(feedQuery).Entries)
      {
        Console.WriteLine(entry.Title.Text);

        DataQuery pageViewQuery = new DataQuery("https://www.google.com/analytics/feeds/data");
        pageViewQuery.Ids = entry.ProfileId.Value;
        pageViewQuery.Metrics = "ga:visits";
        pageViewQuery.GAStartDate = DateTime.Now.AddMonths(-1).ToString("yyyy-MM-dd");
        pageViewQuery.GAEndDate = DateTime.Now.ToString("yyyy-MM-dd");
        foreach (DataEntry pvEntry in service.Query(pageViewQuery).Entries)
        {
          Console.WriteLine(pvEntry.Metrics[0].Value);
        }

      }
      Console.ReadLine();
    }
  }

Sunday, November 14, 2010

Embedded Analytics

I’ve been working on a website for somebody and they wanted to show some information about the number of visitors to the site on the site itself. Since we already had Google Analytics installed, the obvious solution was something that hooked into that. I knew there was a Google Analytics API which I could hook into, but being lazy I hoped somebody else had already implemented something for me.

Luckily for me, there is a company called Embedded Analytics that do provide this kind of service. There are various free tools available although it does cost money if you want to use it for multiple sites. But the free service was good enough for my current needs.

Setup was simple, although you do obviously need to let them have access to your Google Analytics data. If you don’t feel comfortable with that then this isn’t the service for you. If you are OK with that, then you can quickly knock together a chart showing the number of visitors or page views in the recent past. Another nice little tool is the ability to get email alerts when somebody has linked to your site.

And now I think I might want to add this to other sites, but it’s going to start costing money to use on more than one site. When my laziness is confronted by my tight fistedness, my tight fistedness always wins out. So now I have to go back to that API and figure out how to do all this and more myself.

Thursday, November 11, 2010

Creating a valid file name in C#

This was new to me so I thought I’d share. If you want to generate a file name based on some text, but that text may contain characters that aren’t allowed in file names, what to do? In the past, I’ve fumbled around, replacing invalid characters as bugs have presented themselves. But there is a much more robust way, once I noticed the Path.GetInvalidFileNameChars method. That led to this very simple method.

    private string ReplaceInvalidFileNameChars(string fileName)
    {
      foreach (char invalidChar in Path.GetInvalidFileNameChars())
      {
        fileName = fileName.Replace(invalidChar, '_');
      }

      return fileName;
    }

Sunday, November 07, 2010

Styled maps–Showing urban areas

Styled Google Maps but I haven’t found a use for them up until now. But I had a desire to view urban areas in the UK and realised I could do that with styled maps, so here it is.

Loading map...

Tuesday, October 12, 2010

Uploading files and folders to a specific Google Docs folder using the .NET API

The .NET API for Google Docs is a strange beast. First, it has a DocumentsRequest class and a DocumentsService class. Sometimes you’ll need to use one class, sometimes the other and it’s not clear what the distinction is between the two. And I’d assumed the .NET API supported everything you could do with the raw HTTP API and hence I thought uploading a file or folder to a specific folder wasn’t possible. This led to further confusion when I tried to move a folder and it did move but left the original folder behind and I couldn’t figure out how to delete that folder. And as with most things from Google, the documentation is somewhat lacking.

So I eventually figured out I’d have to do a little work myself and came up with a couple of extension methods, one to create a folder and one to upload a file, but both accepting a parent folder.  When I say I needed to do a little work, what I really mean is I used Reflector to see how the current methods worked and employed some copy and paste…

  public static class GoogleDocsExtensions
  {
    public static Document CreateFolder(this DocumentsRequest docsRequest, string folderName, Document parentFolder)
    {
      Document doc = new Document();
      doc.Type = Google.Documents.Document.DocumentType.Folder;
      doc.Title = folderName;
      if (parentFolder == null)
        return docsRequest.Insert<Document>(new Uri(DocumentsListQuery.documentsBaseUri), doc);
      else
      {
        return docsRequest.Insert<Document>(new Uri(parentFolder.AtomEntry.Content.AbsoluteUri), doc);
      }
    }

    public static void UploadFile(this DocumentsService docsService, string file, Document folder)
    {
      FileInfo info = new FileInfo(file);
      using (FileStream input = info.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
      {
        Uri uri = new Uri(folder.AtomEntry.Content.AbsoluteUri);

        string str = info.Extension.ToUpper().Substring(1);
        string contentType = (string)DocumentsService.DocumentTypes[str];
        docsService.Insert(uri, input, contentType, info.Name);
      }
    }
  }

Sunday, October 10, 2010

Installing PHP 5.2 on Windows 7 IIS 7.5

I had a bit of fun upgrading my Windows 7 machine to PHP 5.2 the other day. There’s an installer available that goes most of the way but didn’t quite work for me (there’s also an installer available on www.php.net, I’m not sure how they differ).

The first problem was that my installation of IIS didn’t have the FastCGI module installed. This was pretty easy to rectify, in the IIS administrator go to Modules and then click the ‘Configure Native Modules’ link and then click the ‘Register’ button. The FastCGI module will live somewhere like ‘C:\Windows\System32\inetsrv\iisfcgi.dll’. Then I manually added a handler for PHP files, although if you add the module with the correct ‘FastCgiModule’ name, a re-install should correctly add the handler.

After this was set up, I was able to load up PHP pages but they weren’t being rendered properly, because it looked like the PHP code wasn’t being evaluated and was just getting outputted as plain text. This confused me for a while and I thought the installer hadn’t done its job. But after firing up Fiddler, I saw this header in the response from the server – X-Powered-By: PHP/5.2.14, which suggested the file was getting handed to PHP which was doing something with it. So I guessed there must be a problem somewhere in the PHP config file, php.ini. After searching around in there, I discovered this setting - short_open_tag = Off. This means any PHP files that use <? as their opening tag instead of <?php will not get evaluated, and turning that option on fixed my problems. One other thing to note is that an IIS reset seems to be required for any changes to that config file, since when using FastCGI the PHP executable stays resident in memory rather than restarting for every request.

Tuesday, October 05, 2010

WinForms ProgressBar with text

Progress bar with textSomebody asked how to use my transparent label on top of a ProgressBar control, since it disappeared when the progress bar changed its state. I had a look at it, but the transparent label wasn’t getting any notification of changes, even when I hooked into the WndProc method. So I wondered if it would be possible to sub-class the ProgressBar control and write some text on top of it that way. And that seems to work OK, as shown below.

Update – I’ve fixed the flickering

Update 2 – It now picks up the text colour from the ForeColor property

Update 3 – Should now work on Windows XP

Update 4 – Now lets you specify the font used for the text

using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using System;

namespace WinFormControls
{
 
  public class TextProgressBar : ProgressBar
  {
    protected override CreateParams CreateParams
    {
      get
      {
        CreateParams result = base.CreateParams;
        if (Environment.OSVersion.Platform == PlatformID.Win32NT
            && Environment.OSVersion.Version.Major >= 6)
        {
          result.ExStyle |= 0x02000000; // WS_EX_COMPOSITED 
        }

        return result;
      }
    }

    protected override void WndProc(ref Message m)
    {
      base.WndProc(ref m);
      if (m.Msg == 0x000F)
      {
        using (Graphics graphics = CreateGraphics())
        using (SolidBrush brush = new SolidBrush(ForeColor))
        {
          SizeF textSize = graphics.MeasureString(Text, Font);
          graphics.DrawString(Text, Font, brush, (Width - textSize.Width) / 2, (Height - textSize.Height) / 2);
        }
      }
    }

    [EditorBrowsable(EditorBrowsableState.Always)]
    [Browsable(true)]
    public override string Text
    {
      get
      {
        return base.Text;
      }
      set
      {
        base.Text = value;
        Refresh();
      }
    }

    [EditorBrowsable(EditorBrowsableState.Always)]
    [Browsable(true)]
    public override Font Font
    {
      get
      {
        return base.Font;
      }
      set
      {
        base.Font = value;
        Refresh();
      }
    }
  }
}