0

What I'm trying to do seem simple but the implementation is throwing me. How can I force a label to refresh after I change it's value?

I've just created a simple progress window. As the main app is doing something I pop up the progress window and I want to update the values of the label and the progress bar as the loop executes.

I created a new NSWindow and XIB file that has outlets to the label and the progressbar and I'm loading it with this:

_pbWindow                           = [[ProgressBar alloc] initWithWindowNibName:@"ProgressBar"];

[_pbWindow showWindow:self];

When I call:

_pbWindow.lblProgress1.stringValue = @"Doing this now...";

I can't get the label to actually refresh in the new window.

I have searched here and google and can't seem to find anything to tell me how to do this in a different window, only that if it's in the main window that it will just refresh (as has always been my experience anyway).

I thought maybe it was because the window didn't have focus so I tried opening it modally:

[[NSApplication sharedApplication] beginSheet:[_pbWindow window]
                               modalForWindow:[[NSApplication sharedApplication] mainWindow]
                                modalDelegate:_pbWindow
                               didEndSelector:nil
                                  contextInfo:nil];

And still it doesn't work.

C4W
  • 239
  • 4
  • 13

1 Answers1

2

When you set a text field's stringValue, the text field will mark itself as needing display (one of the -setNeedsDisplay... methods). It does not immediately display itself. Later, during the main event loop, the application will tell each of its windows to -displayIfNeeded, and the new content of the text field will actually be drawn on screen.

However, since you're in a long-running loop, you're not allowing the main event loop to run and the windows don't get an opportunity to display if needed.

One workaround is to directly invoke -displayIfNeeded on the text field after you change its stringValue:

_pbWindow.lblProgress1.stringValue = @"Doing this now...";
[_pbWindow.lblProgress1 displayIfNeeded];

However, there are other problems with not letting the main event loop run. Just working around this one problem still leaves those others. For example, your app will show as Not Responding in Activity Monitor and the system will show the spinning colorwheel cursor when the mouse is over its windows. You should consider finding another way to structure your app.

For example, you can run your progress window as a modal window on the main thread (using -[NSApplication runModalForWindow:]) and do the work on a background thread. When the work completes, invoke one of the -[NSApplication stopModal...] methods or -[NSApplication abortModal] (but read the documentation to see under what circumstances you should use which, although some restrictions on -stopModal... were lifted in 10.9). The main thread would be allowed to go back to the main event loop, but the modal window would prevent the user from doing anything with the rest of the UI that they shouldn't be allowed to do while the operation proceeds.

Alternatively, maybe the operation should not actually monopolize your entire app. Maybe it should just be modal to a single window. In that case, you can show the progress window as a sheet on the window which should be disabled for the duration of the operation. Begin the sheet using -[NSWindow beginSheet:completionHandler:] and start the operation in the background. When the operation completes, invoke one of the -[NSWindow endSheet:...] methods on the main thread.

If you run the work on a background thread, always be sure to shunt any changes that will affect the GUI to the main thread using GCD and dispatch_get_main_queue() or -performSelectorOnMainThread:....

Ken Thomases
  • 88,520
  • 7
  • 116
  • 154
  • Thank you very much for a fantastic answer! Both solutions work well so I'm not sure which I'll end up with but for now I'm using the displayIfNeeded method to see if that satisies the need. – C4W Jan 18 '15 at 15:33