I have an application with some charts and many comboboxes (activex control).
When the user changes the value of any combobox the charts are updated. No problem here.
So, I made a code to export the whole screen of the app as image. This is used to simulate several scenarios.
But, here is where the problem begins.
There are some “for…next” loops in this code to change the values of these comboboxes. When the images are exported, the charts are updated as expected, but the comboboxes DO NOT change their values. They show the same value on every scenario, even that the charts are being updated.
So, the question is: Is there a way to refresh the value of a combobox before the code is ended?
Sub example() For Each elem In myArray Sheets("App").ComboBox1.Value = elem Sheets("Temp").Shapes.AddChart Set cht = Sheets("Temp").ChartObjects(1) Sheets("App").Range("A1:AM103").CopyPicture Appearance:=xlScreen, Format:=xlBitmap With cht.Chart .Paste .export Filename:="test.jpg", FilterName:="jpg" .Parent.Delete End With Next End Sub
First of all, congratulations: you’ve found a very annoying bug here.
I’ve tried to reproduce your issue, and I can do it very easily.
- If you set a breakpoint after updating the combobox (i.e. the thread is paused) => the ActiveX component is refreshed
- If you set an
Application.Wait (TimeSerial(Hour(Now()), Minute(Now()), Second(Now()) + 5))(i.e. you visually stop the execution for 5 seconds, but technically speaking the thread is still running) => you can clearly see the ActiveX component not getting updated, and that’s why your image is generated wrongly.
I’ve tried all the obvious tricks (
Application.ScreenUpdating = True,
Application.EnableEvents = True,
Application.Calculate etc.) but no success in any case.
It really seems that ActiveX components will get refreshed by Microsoft Excel only when the VBA thread ends. Wow.
The only way around this bug that I can think of
The only way I can think of to technically stop the execution after updating the ActiveX component, and resume it later, is to use the
Application.OnTime method of Excel:
Application.OnTimeschedules a procedure to be run at a specified time in the future (either at a specific time of day or after a specific amount of time has passed).
For how ugly it can look from a technical point of view, you could update your combobox and then schedule the rest of your code to happen a second after you did it. From a technical point of view:
- VBA thread 1: updates your ComboBox and ends => the ActiveX component gets refreshed
- 1 second pause without VBA threads.
- VBA thread 2: creates the chart and export the image using the updated ActiveX component.
Practically, your code would look something like this:
Dim myArray(2) 'declare your array as global so that it can be accessed by all the macros - in my example I assume it contains 3 elements Dim currentElem As Integer 'declare this index as global so it remains in memory even after the code ended execution Sub example() 'call this macro. 'you first initialize your values: myArray(0) = "test 1" myArray(1) = "test 2" myArray(2) = "test 3" currentElem = 0 'and then call the first update of your activeX component first_step_set_activeX End Sub Sub first_step_set_activeX() If currentElem < UBound(myArray) Then 'for each element not treated yet '(that's why the If currentElem < UBound(myArray) elem = myArray(currentElem) 'get current element from array Sheets("App").ComboBox1.Value = elem 'update your ActiveX component currentElem = currentElem + 1 'increase the currentElem index Application.OnTime TimeSerial(Hour(Now()), Minute(Now()), Second(Now()) + 1), "second_step_make_chart_and_print" 'schedule the call of the printing part End If End Sub Sub second_step_make_chart_and_print() 'here do the job of the printing part Sheets("Temp").Shapes.AddChart Set cht = Sheets("Temp").ChartObjects(1) Sheets("App").Range("A1:AM103").CopyPicture Appearance:=xlScreen, Format:=xlBitmap With cht.Chart .Paste .Export Filename:="test.jpg", FilterName:="jpg" .Parent.Delete End With 'and reschedule the call for the next activeX component Application.OnTime TimeSerial(Hour(Now()), Minute(Now()), Second(Now()) + 1), "first_step_set_activeX" End Sub