Charts aren't rendered in MS Access forms - vba

I created a form that contains 5 charts. When opening, the cursor changes 5 times, so obviously the data is queried, but the charts aren't rendered and left blank. However, when moving the form outside of the screen and back in, parts of the chart are shown, so it seems that is just a repaint missing.
I tried to execute me.chart1.repaint on several events, but I haven't found the appropriate event and I am not sure if me.chart1.repaint is actually necessary.

Private Sub chart1_Updated(Code as integer)
me!chart1.Visible = True
End Sub
Don't worry if visible already true!

I am using this code as a workaround:
Private Sub chart1_Updated(Code As Integer)
DoCmd.Minimize
[Forms]![myform].SetFocus
DoCmd.Restore
End Sub

Sometimes the form is doing too many things while rendering the graphs, and the actual painting does not have time to finish before processing moves on the next object. You can force Access to "catch a breath" like this:
Private Sub Detail_Paint()
DoEvents
End Sub
Since the Detail section of the form is doing the actual rendering, it has a chance to finish each object before moving on to the next.

Related

How to stop Access from prompting "Do you want to save changes to the layout of query"

How can I stop Access from prompting "Do you want to save changes to the layout of query" when I try to close a form that has a subform in datasheet view?
I have a form with a subform in Datasheet view with the .SourceObject set to a temporary pivot / crosstab query (no actual form object). If the user changes the width of a column and closes the window with the built-in Access close button, the user (and annoyed developer) is always presented with the "Do you want to save changes to the layout of query" prompt.
I am able to avoid this prompt by setting MySubform.SourceObject = "" in the click of my Close button but anyone clicking the [x] button or pressing CTRL+F4 or CTRL+W gets the prompt.
I have put breakpoints in the Form_Close and Form_Unload events but this prompt appears before they fire.
I want to clarify further that this subform object is Unbound and not based on a form object. I am building a dynamic Crosstab SQL statement, creating a QueryDef with the SQL, and then setting MySubform.SourceObject = "query.qry_tmp_Pivot" This is a neat technique for a crosstab/pivot query because the columns can vary and I don't think a form object would support that.
I am able to use the Watch window on the MySubform object and I can see that a MySubform.Form object exists. It has a Controls collection containing a control for all of the columns in my query. I have an optional routine that I can run that will loop through all of the controls and set their .ColumnWidth = -2, which will auto-size the column width based on the widest data of the visible rows in the datasheet. When this routine was running I was noticing that every time I closed the form (not using my Close button) I was getting the save prompt. I have disabled this routine for debugging but a user will still get the prompt if they manually adjust any column width.
I feel like I need to explain this extra detail so you realize this is not an Access 101 issue. You probably know that if you've read this far. Here's another thought I had: Maybe I could trap the Unload event in the subform control before the prompt happens. Because there is no true Form object to put test code in, I created a class object and passed MySubform to it. The class uses WithEvents and creates events like OnClose and OnCurrent on the class module's mForm object. Sample class code is below.
Private WithEvents mForm As Access.Form ' This is the form object of the Subform control
Public Sub InitalizeSubform(Subform As Access.Subform)
Set mForm = Subform.Form
Debug.Print Subform.Name, mForm.Name, mForm.Controls.count
' Create some events to intercept the default events.
mForm.OnClick = "[Event Procedure]"
mForm.OnClose = "[Event Procedure]"
mForm.OnUnload = "[Event Procedure]"
mForm.OnCurrent = "[Event Procedure]"
End Sub
Private Sub mForm_Click()
Debug.Print "Clicking " & mForm.Name
End Sub
Private Sub mForm_Current()
Debug.Print "On Current " & mForm.Name, "Record " & mForm.CurrentRecord & " of " & mForm.RecordsetClone.RecordCount
End Sub
Private Sub mForm_Unload(Cancel As Integer)
Debug.Print "Unloading " & mForm.Name
End Sub
Private Sub mForm_Close()
Debug.Print "Closing " & mForm.Name
End Sub
The VBE Watch window shows my new events on the mForm object but unfortunately they never fire. I know the class works because I used it with a bound subform and all of the events are intercepted by the class. I'm not sure what else to try.
Events on the subform never fire because it's a lightweight form (without a module). See this Q&A and the docs. Lightweight forms don't support event listeners, but do support calling public functions from an event, e.g. mForm.OnClick = "SomePublicFunction()"
Note that the workaround described in this answer also opens up the possibility of displaying a crosstab query in a form without saving it at all.
Alternatively, you could try capturing the event on your main form, and suppress saving there.
I gave answer credit to #ErikA for this question. He directed me to an answer to a somewhat related question here which is what I ended up implementing and it works. His instructions were very straightforward and easier to implement than I first anticipated.
The answer to my question seems to be that it may not be possible to avoid the save prompt when using a lightweight form object. If someone comes along with a solution I'd still like to hear it.
What I learned from this experience:
An unbound subform that has its .SourceObject set to a table or query will not have a code module. This considered to be a lightweight object.
Public functions can be added to event properties on lightweight forms but they don't seem to fire before the save prompt appears as the parent form is closing.
Binding a pivot query or temp table to a real form object that has 254 controls on it is the only way I found that doesn't prompt to save design changes when the parent form closes. Mapping the query/table columns to the 254 datasheet controls is super fast.
A class is a nice way to intercept events if you're not using a lightweight object. A class also lets you open multiple instances of the same object.
I ended up not implementing my solution without using a class because there were no events I needed to capture. At this point I don't need my frmDynDS to be used for other purposes and I'd like to keep the object count down (this is a large app with 1400+ objects).
I put the following code in a function that gets called when the subform is loaded or refreshed.
With subCrosstab
' Set the subform source object to the special Dynamic Datasheet form. This could be set at design time on the Subform control.
.SourceObject = "Form.frmDynDS"
' Run the code to assign the form controls to the Recordset columns.
.Form.LoadTable "qry_tmp_Pivot" ' A query object works as well as a table.
' Optional code
.Form.OnCurrent = "=SomePublicFunction()"
.Form.AllowAdditions = False
.Form.AllowEdits = False
.Form.AllowDeletions = False
.Form.DatasheetFontHeight = 9
End With
I can now resize the columns and never get a prompt to save layout changes no matter how I close the parent form.

Show progress on UserForm without percentage update

I want to show progress while my program is doing everything it does, to know that it is working and not failed. Problem is, I don't use any index, For...Next, While, or anything I can use to call my progress-bar update.
The program:
opens 2 books
copies a bunch of rows from one to another and
refreshes all the data/graphics in many other sheets.
As I don't have a loop through all the rows (which was my main reference point for progress-bar), I'm not sure how I can show the progress.
Sorry if I can't be more specific but I'm not sure if I can share any of the code. If you need more information I could try to post some in here.
My progress-bar code is:
Public Sub UpdateProgressBar(PctDone As Single)
With ProgressForm
' Update the Caption property of the Frame control.
.FrameProgress.Caption = Format(PctDone, "0%")
' Widen the Label control.
.LabelProgress.Width = PctDone * (.FrameProgress.Width - 10)
End With
' The DoEvents allows the UserForm to update.
DoEvents
End Sub
Public Sub UpdateProgressText(txt)
ProgressText.Caption = txt
End Sub

Get object name in callback function for VBA object?

Let's say I have a userform with a command button named cmd_1.
For purposes of my application, I have a lot of validation I would like to do on this click - see this question for some discussion about what I am attempting to do. Basically I am hoping to implement a single callback function with logical rules I check, based on the calling object's name, to determine the eligibility of that control's actions.
This would look something like:
private function isValid(p_controlName as string) as boolean
'logical checks based on the the value of p_controlName
'returning true/false as appropriate
end function
and I would be using it something like this
Private Sub cmd_1_Click()
if isValid("cmd_1") = false then exit sub
End Sub
Now I am going to be putting this into a lot of UI callbacks (I don't like thinking about how many). I would rather avoid having to tediously add the name of the control to each callback. I would very, VERY much prefer do something like:
Private Sub cmd_1_Click()
'this is not valid syntax
if isValid(ThisControl.name) = false then exit sub
End Sub
This is better for a whole variety of reasons, ease of implementation, consistency, less likely for errors, etc.
However I cannot seem to find a way to get the name of the calling control within a callback function.
Is there some way to get the name of a control in VBA code from a callback function to use as an argument like I am trying?
If it's only button click event handlers, then Screen.ActiveControl.Name should get you what you want - Screen.ActiveControl returns the control with the keyboard focus, and a command button takes the keyboard focus on being clicked.
Alternatively, use a callback function for each, with a parameter based on the control name or information that you need, and make the controls each have:
Dim rect As Control
Dim coll As Form
Set coll = Forms("myForm")
For Each rect In coll.Controls
If rect.ControlType = acRectangle Then
rect.OnClick = "=myCallbackFunction(" & param & ")"
End If
Next rect
myCallbackFunction is just a function on the same form as the control, or a public function in a module.
I've done this where each rectangle (up to 100, pre-generated at height 0 on my form) is moved and made visible based on a recordset, then they have this onclick event setup based on an ID from the recordset. In my case it selects a different subform record to match the rectangle clicked.
Don
Have a great day

VBA Listbox becomes unresponsive after first use

I have a VBA (Excel 2010) system which involves selecting an item from a listbox and then displaying it in another form. Here is a very simplified version of what happens.
' Part of frmForm1 code module
sub lstListbox_Click
dim MyEvent as string
dim i as integer
i=me.lstListbox.listindex
MyEvent=me.lstlistbox.list(i)
' Now show the item in the second form
Load frmForm2
me.hide
ThisWorkbook.LoadDataIntoForm2 (frmForm2, MyEvent)
frmForm2.show
unload frmForm2
me.show
end sub
The listbox accepts the click, and first the event (the event handler is giver above). Key parts of the event handler are:
Load the second form (to display the detail data)
Pass the second form as a UserForm parameter to a procedure (LoadDataIntoForm2)
Hide the host form (frmForm1) and show the second form (frmForm2)
When the second form processes an Exit click, the code looks like this:
' Part of frmForm2 code module
sub cmdExit_Click
me.hide
end sub
The first time round it works fine - but when I return to frmForm1 (in the tail end of the lstListBox_Click procedure), even though the rest of the form is operative, the listbox remains stubbornly unresponsive.
I've managed to abstract this down to a little demo system if that would help - the same behavior is seen there. (It's regular .xls file, but that seems not to be easily acceptable as an upload)
Has anyone seen this before? And does anyone have any ideas how I might get this to work the way I want it to?
Thanks,
Tony
The default for the .Show method is to make the form modal. Explicitly set it to modeless:
Sub lstListbox_Click
...
Me.Show vbModeless
End Sub

Is it better to show ProgressBar UserForms in VBA as modal or modeless?

Is it better to show ProgressBar UserForms in VBA as modal or modeless? What are the best practices for developing progress indicators in VBA?
Modeless UserForms require the use of Application.Interactive = False, whereas Modal UserForms by their very nature block any interaction with the application until the core procedure has finished, or is cancelled.
If Application.Interactive = False is used, however, the Esc key interrupts code execution, so the use of Application.EnableCancelKey = xlErrorHandler and error handling (Err.Number = 18) is required in both the UserForm and the calling procedure.
Resource intensive calling procedures can also result in CommandButton_Click and UserForm_Activate events misfiring in modeless UserForms.
In general, progress indicators that use modal UserForms seem simpler, because the code that is being executed is fully contained in the UserForm module, and there is less need for passing of variables.
The problem, however, with using modal UserForms for progress indicators is that a separate UserForm module is required for every procedure that needs a progress indicator, because the calling procedure has to be inside the UserForm_Activate procedure.
So, while it is possible to have a single reusable progress indicator in a modeless UserForm, it will be less reliable than executing the code from within multiple modal UserForms.
Which way is better?
Thanks!
There's also a third way, using the Application.StatusBar.
You can even simulate a true progress bar by using a sequence of U+25A0 and U+25A1 characters.
I am going to close this one out and say Modal is the winner. I have tried both ways, but you end up trying to close too many loopholes with modeless userforms. Modal is more difficult because it is more strict, but it encourages you to break up your code into smaller chunks which is better in the long run anyway.
Definately Modal.
If you are going to consider Modeless, you ought to run it on a seperate out-of-process thread and not on the Excel.exe main thread.
I think that the initial topic is worth of replying since the question was formulated so nicely that google finds it first.
Section 1 - Theory
The first thing to say is that to transfer the variables between the modules is not difficult at all.
The only thing you need to do is to create a separate module and put there all the global variables. Then you will be able to read them everywhere in all forms, sheets, modules.
The second thing is the window should be a MODELESS. Why that?
The answer is to keep the mobility of the code, i.e.
the function where the most routine process is executed is not to be located in the UserForm module
you can call the window with progress bar from everywhere and
the only connection between the routine function/procedure are the global variables
This is a great advantage to be versatile here.
Section 2 - Practice
1) Create a module "Declaration" with the global variables:
Public StopForce As Integer
'this variable will be used as an indicator that the user pressed the cancel button
Public PCTDone As Single
' this is the % of the work that was done already
Public CurrentFile As String
' any other parameter that we want to transfer to the form.
2) Create the form with the button. In OnClick event of the button there should be a code where we refer to the global variable StopForce in Declaration module
Private Sub CommandButton1_Click()
Declaration.StopForce = 1
End Sub
3) Add one procedure where you update the progress bar
Sub UpdateProgressBar(PCTDone_in As Single)
With UserForm1
' Update the Caption property of the Frame control.
.FrameProgress.Caption = Format(PCTDone_in, "0%")
' Widen the Label control.
.LabelProgress.Width = PCTDone_in * _
(.FrameProgress.Width)
' Display the current file from global variable
.Label1.Caption = Declaration.CurrentFile
End With
End Sub
4) in any other module we must have the functions or the procedure/sub where the routine is done:
For i=1 to All_Files
Declaration.CurrentFile = myFiles (i)
FormFnc.UpdateProgressBar (i / .Range("C11").Value)
DoEvents
If Declaration.StopForce = 1 Then
GoTo 3
End If
Next i
Actually you have following properties, resulting in pros/cons depending on your need:
Type | Impact on UI | Impact on caller execution
----------|--------------|-----------------------------
Modal | Blocked | Blocked until Form is closed
Modeless | Not blocked | Continues
If you want to block the UI AND let the caller continue, then you need to open the Form in modal mode with Application.OnTime.