Link to home
Start Free TrialLog in
Avatar of shifty_mc
shifty_mc

asked on

How to tell if a Windows Form has painted itself COMPLETELY

Hi,

I'm trying to capture the screen of a Form which contains a browser, which works fine if I do it manually (say once you press a button or something).

The problem is how to do this programatically as the browser is opening a web page which could take a little while, and the DownloadComplete event seems to fire too early (ie it just captures a blank screen - presumably a millisecond or so before the screen properly paints.

I've tried other events, both in the form and the contained browser - none of which seem to be fired at just the right moment when the screen displays the information. Thread.Sleep() doesn't work either as it delays everything.

Is there an event I've missed which will go at the right time? Or a workaround using threads?

I've also looked at intercepting WM_PAINT messages, but couldn't get it working.

Any ideas? Cheers.
Avatar of eternal_21
eternal_21

What browser component are you using?
Avatar of shifty_mc

ASKER

Ummm...

private AxSHDocVw.AxWebBrowser axWebBrowser1;

that answer your question? It's just the 'Microsoft Web Browser' that you can add to the toolbox in Visual Studio.
ASKER CERTIFIED SOLUTION
Avatar of eternal_21
eternal_21

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Ok, I changed from Navigate to Navigate2 and DownloadComplete to NavigateComplete2 but still no joy :-(

If it helps, I've overriden wnproc in order to log the events, and this is the result (the messages are ints - I've just spent a little time looking to find out which ones are which with no luck - I'll post this and carry on - don't know if I'm looking in all the wrong places- they must be out there somewhere!)

Loading Browser                                       (in Form_Load)
Navigating to file://C:\webpagetest.html    (still in Form_Load and calling Navigate2() method)
Download complete event firing!
Event firing: 24
Event firing: 70
Event firing: 70
Event firing: 28
Event firing: 134
Event firing: 6
Event firing: 133
Event firing: 20
Event firing: 71
Event firing: 5
Event firing: 3
Event firing: 127
Event firing: 127
Event firing: 127
NavigateComplete2 Event Firing!!
Sleeping Thread
Back from sleeping thread
Calling Application.DoEvents()
Event firing: 15
Back from Application.DoEvents()
Calling capturescreen method
Calling SendMessage
Event firing: 791
Event firing: 175
Event firing: 20
Event firing: 792
Back from SendMessage
Saving... test.bmp
Back from capturescreen method
Event firing: 132
Event firing: 132
Event firing: 32
Event firing: 49721
Event firing: 512
Event firing: 675
Event firing: 134
Event firing: 6
Event firing: 28
Event firing: 70
Event firing: 28
Event firing: 134
Event firing: 6
Event firing: 134
Event firing: 6
Event firing: 28

The Thread.Sleep() bit you mentioned still holds up everything as it did with DownloadComplete. The NavigateComplete2 is fired later than DownloadComplete as you can see, which is good.  Also, the capture screen method works by using the line
SendMessage(hWnd, Win32.WM_PRINT, hDCMem, new IntPtr(Win32.PRF_CHILDREN | Win32.PRF_CLIENT | Win32.PRF_ERASEBKGND | Win32.PRF_NONCLIENT | Win32.PRF_OWNED));
which is obviously creating a PRINT event - so don't know if this is messing things up.

Thanks
I *did* test this code, at first it didn't work.  Then I added the Application.DoEvents() after the call to Thread.Sleep.  It worked fine for me.

Try this for your form capture?

    [System.Runtime.InteropServices.DllImport("gdi32.dll")] private static extern bool BitBlt(IntPtr hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, System.Int32 dwRop);

    static Bitmap CaptureForm(System.Windows.Forms.Form form)
    {
      Graphics g1 = form.CreateGraphics();
      Bitmap bitmap = new Bitmap(form.ClientRectangle.Width, form.ClientRectangle.Height, g1);
      Graphics g2 = Graphics.FromImage(bitmap);
      IntPtr dc1 = g1.GetHdc();
      IntPtr dc2 = g2.GetHdc();
      BitBlt(dc2, 0, 0, form.ClientRectangle.Width, form.ClientRectangle.Height, dc1, 0, 0, 13369376);
      g1.ReleaseHdc(dc1);
      g2.ReleaseHdc(dc2);
      return bitmap;
    }

And here was my code:

  private void Form1_Load(object sender, System.EventArgs e)
  {
    object o = new object();
    object url = "http://www.yahoo.com/";
    this.axWebBrowser1.NavigateComplete2 += new AxSHDocVw.DWebBrowserEvents2_NavigateComplete2EventHandler(this.axWebBrowser1_NavigateComplete2);
    this.axWebBrowser1.Navigate2(ref url, ref o, ref o, ref o, ref o);
  }

  void axWebBrowser1_NavigateComplete2(object sender, AxSHDocVw.DWebBrowserEvents2_NavigateComplete2Event e)
  {
    System.Threading.Thread.Sleep(250);
    Application.DoEvents();
    System.Drawing.Bitmap bitmap;
    bitmap = CaptureForm(this);
    bitmap.Save("c:\\test.bmp", System.Drawing.Imaging.ImageFormat.Bmp);
  }
By the way, how did you log the form events using WndProc?  (Just curious, I have been trying to do that for a while).
Also, it looks like "Event 15" is significant, don't you think?
Can you post that trace again WITHOUT the DoEvents, and see where that Event 15 is sitting?
yeah, I saw that but couldn't work out what it was, hold on a minute - I've just tried what you said and am organising in order to post back....
Right,

Unfortunately I can't use BitBlt because I'm capturing a screen that might not be visible, so I have to use SendMessage (as I said before I know it works because I've tried implementing it by clicking a button when the form is half off the screen and it's fine).
The whole code for the method I use is...

public Bitmap GrabWindow(IntPtr hWnd)
{
      Class1.Log("Capturing Browser Screen...");
      IntPtr hBmp;
      Bitmap MyBitmap=null;

      IntPtr hDCMem = Win32.CreateCompatibleDC((IntPtr)null);
      Rectangle rect = new Rectangle(this.Left,this.Top, this.Width,this.Height);
      IntPtr hDC = Win32.GetWindowDC(hWnd);
      hBmp = Win32.CreateCompatibleBitmap(hDC, rect.Width, rect.Height);
      Win32.ReleaseDC(hWnd, hDC);
      Graphics grfxScreen = Graphics.FromHwnd(hWnd);
      MyBitmap = new Bitmap((int) this.Width, (int) this.Height, grfxScreen);
      Graphics grfxBitmap = Graphics.FromImage(MyBitmap);
      IntPtr hdcScreen = grfxScreen.GetHdc();
      IntPtr hdcBitmap = grfxBitmap.GetHdc();
      IntPtr hOld = Win32.SelectObject(hDCMem, hBmp);
      Class1.Log("Calling SendMessage");
      Win32.SendMessage(hWnd, Win32.WM_PRINT, hDCMem, new IntPtr(Win32.PRF_CHILDREN | Win32.PRF_CLIENT | Win32.PRF_ERASEBKGND | Win32.PRF_NONCLIENT | Win32.PRF_OWNED));
      Class1.Log("Back from SendMessage");
      Win32.SelectObject(hDCMem, hOld);
      Win32.DeleteObject(hDCMem);
      MyBitmap = Bitmap.FromHbitmap(hBmp);

      Class1.Log("Saving... test.bmp");                  
      MyBitmap.Save("C:\\test.bmp");
      return MyBitmap;                  
}

(The Win32 class is just where I stored all the dllimport stuff)
I realise there's a lot of calls using these dllimports, but I've tried it with c# graphics and just couldn't get it to work - no idea why - it just returned a solid black bitmap.

Other than that the code's pretty identical. The Class1.Log stuff is just a quick method I created to write anything I want to my log.txt file, and to log the events I just used...

protected override void WndProc(ref Message m)
{
      Class1.Log("Event firing: " + m.Msg);
      base.WndProc(ref m);
}

And finally, I just ran it without the DoEvents and got the log...

Loading Browser
Navigating to file://C:\webpagetest.html
Event firing: 24
Event firing: 70
Event firing: 70
Event firing: 28
Event firing: 134
Event firing: 6
Event firing: 133
Event firing: 20
Event firing: 71
Event firing: 5
Event firing: 3
Event firing: 127
Event firing: 127
Event firing: 127
NavigateComplete2 Event Firing!!
Sleeping Thread
Back from sleeping thread
Calling capturescreen method
Capturing Browser Screen...
Calling SendMessage
Event firing: 791
Event firing: 175
Event firing: 20
Event firing: 792
Back from SendMessage
Saving... test.bmp
Back from capturescreen method
Event firing: 15
Event firing: 134
Event firing: 6
Event firing: 28
Event firing: 70
Event firing: 28
Event firing: 134
Event firing: 6
Event firing: 134
Event firing: 6
Event firing: 28
Event firing: 70
Event firing: 133
Event firing: 20
Event firing: 71
Event firing: 28
Event firing: 134
Event firing: 6
Event firing: 15
Event firing: 132
Event firing: 132
Event firing: 32
Event firing: 49721
Event firing: 512
Event firing: 675
Event firing: 132
Event firing: 132
Event firing: 32
Event firing: 49721
Event firing: 512
Event firing: 675
Event firing: 134
Event firing: 6
Event firing: 28
Event firing: 70
Event firing: 28
Event firing: 134
Event firing: 6
Event firing: 134
Event firing: 6
Event firing: 28
Oh, and I call it from NavigateComplete2 using
image = GrabWindow(this.Handle);
I believe the 15 event is WM_PAINT, but I'm not exactly sure what that means - surely it should work as in the original log post which includes the DoEvents, because it paints before it captures the screen?

Phew, I don't know. This is a long mission!
I found a webpage that says there are some problems with NavigateComplete2 firing too early, and explains how to fix it in C++ using BEGIN_DISPATCH_MAP, but I have no idea how this would work in C#, or indeed if this is the actual problem, but here it is anyway...
http://www.codeguru.com/Cpp/I-N/ieprogram/comments.php/c4403/?thread=52026
I think there are other "events" going on when we call Application.DoEvents that are not generating Windows messages. I tried creating a WebBrowser control (inheriting from AxSHDocVw.AxWebBrowser), and monitoring WndProc calls, but was unable to come up with anything consistent.  Any messages related to paint are sent multiple time for a given refresh of a web page - there is no "Totally Painted" message.
I have also found that this method does not work very well (at least on my system) with the AxWebBrowser control.

What is it exactly you are trying to do, generate images of web pages?
Yeah, that's exactly it - generate an image of a webpage.

Ok, I've finally got (after 3 solid days out of my life) what I think is a feasible workaround. It's actually extremely simple - I swear I've gone back more times than I've gone forwards, but this is all a learning process for me so I guess it's good in the long run.

The answer is a combination of the events DocumentComplete and Application.Idle.

When DocumentComplete fires and returns the url that you initially specified (it might fire a few times) you can capture the screen. The advantage of this over NavigateComplete2 is that the final DocumentComplete returns the originally specified url, so you know what to check for, whereas this url is returned in the first NavigateComplete2 (assuming these events are fired a few times - which happened quite often to me with more complicated sites).

And then to combat the delay problem - ie if you tried to capture straightaway, there is a lag between the event firing and the screen being suitable for capture - the Application.Idle event can be used. I only create this when the final DocumentComplete is received, and then I can use...

private void OnIdle( object sender, System.EventArgs e)
{
      CountIdles++;
      if (CountIdles==10)
            image = GrabWindow();
}

This basically just waits a little bit, which I can do because the event is called regularly, not just once.
And finally I got the result I wanted. It's still not perfect, and as I said - the code is horrible, but I don't care any more!

If anyone comes up with a better solution, feel free to point me in the right direction.

eternal_21 - thanks for your help, I'll award the points to you for your time,

Cheers.
Glad you got everything to work out!

For future reference, this site defines the values of many window messages:

http://cvs.sourceforge.net/viewcvs.py/nsis/NSIS/Include/WinMessages.nsh?rev=1.2

So, 15 (0xF - the table is in hex) is indeed WM_PAINT.

:)
shifty_mc, Hi it looks like youve done just what im trying to do,

Save a webpage as a single image on the harddrive, being in mind webpages have different heights, therefore being able to capture the entire page and not just visable window...

I was wondering if you could post the code for your WIN32 class, as im having trouble in working certain elemements of your code out.

Look forward to hearing from you shortly


Carl
shifty, if you could post tips on the Win32 class you're using, it would greatly help me out as well.  

you can email me too: derkrome AT yahoo DOT com

cheers!
After some searching, I found a good Win32 .net library that exposes all of the needed defines & functions (thecodeproject.com/csharp/Win32.asp).

My only difficulty now is that some elements inside the web-control are not being grabbed.  For example, an html file is fine, but if you navigate to an Office file, the capture is all white.

ideas?
Hi,

Sorry I didn't reply before - just wasn't checking my emails properly.

I think the class you found was the exact one I was using - I'm not that experienced a programmer and a lot of my code was amalgamations of other people's.

I'm afraid I only got to where you are now and stopped - I didn't need it for any other purpose other than capturing html.  I believe the problem is because this method isn't the greatest in terms of functionality - it just 'happens to work', if you see what I mean, and doesn't actually deal with the ins and outs. In other words I don't think there's an easy answer, but then I don't know for sure,and people were telling me for ages you couldn't do it with html, so who knows.

In my travels I found a very thorough article on this exact issue on codeproject.com (in fact the only article I found in about a month of doing this and it was published about 2 days after I'd got mine working - typicle).  It's in C++ and details how to do this 'properly' (which appears to be a very big issue indeed - I certainly couldn't understand it (but admittedly didn't look too much into it). However, I don't know if it would be of any use to the specific issue of opening office documents.

http://www.codeproject.com/internet/htmlimagecapture.asp

Hope that helps, good luck