Excel VBA TopLeftCell.Row property inconsistent results - excel

Here's the scenario. When a row is inserted in the worksheet there's a worksheet_change event that calls a sub that inserts five pairs of buttons on the row that was inserted. When one of those buttons is clicked it runs a sub that issues a MsgBox that displays the TopLeftCell.Row and Column of the button. Multiple rows can be inserted, each with its five sets of buttons (ten in total). Any button in any row can be selected at any time.
What I'm seeing is that after I open the workbook and press one of the buttons, the MsgBox always displays the correct column, but it seems to get "stuck” on one particular row no matter which row the button that I'm clicking is actually in. If I delete the row it is stuck on then it gets "stuck" on a different row (as long as it also contains buttons). But not all of the buttons will be stuck to the same row.
If I copy two adjacent rows with buttons that are “stuck” on the same row to another location, those buttons are still stuck together except on a different row.
It seems like there’s a problem with the button collection. If I save the workbook the problem goes away. If I insert a new row and the Add_Buttons routine runs again, the problem reappears but involving different rows. So my button routine is probably leaving something temporary behind that gets cleared up when I do a save.
Here's the code that builds the buttons.
Public activeWS As Worksheet
Public activeRG As Range
Public Sub Add_Buttons(ByVal myRow As Long)
'Add the five sets of Submit IM and Submit Webins buttons to a new row. The code
'uses named ranges to locate the cells where the buttons should be added so that
'new columns can be added to the spreadsheet without requiring changes to the code.
'The headings must be labeled 'IM#' and 'Webins#'.
Dim i As Long
Dim t As Range
Dim btn As Button
'In each range, place the button below the column label.
Application.ScreenUpdating = False
For i = 1 To 5
Set activeWS = Sheet1
Set activeRG = activeWS.Range("Scan" & i & "_Hdngs")
'The start of the range plus the position of the specified column in
'the range gives the absolute column location to add the button
'Create the Submit IM button
nCol = activeRG.Cells(1, 1).Column + findCol("IM#", activeRG) - 1
Set t = activeWS.Range(Cells(myRow, nCol), Cells(myRow, nCol))
Set btn = activeWS.Buttons.Add(t.Left, t.Top, t.Width, t.Height)
With btn
.OnAction = "Create_Primary_IM"
.Caption = "Submit IM"
.Name = "BtnIM" & myRow & i
.Font.Size = 10
End With
'Create the Submit Webins button
nCol = activeRG.Cells(1, 1).Column + findCol("Webins#", activeRG) - 1
Set t = activeWS.Range(Cells(myRow, nCol), Cells(myRow, nCol))
Set btn = activeWS.Buttons.Add(t.Left, t.Top, t.Width, t.Height)
With btn
.OnAction = "Create_Primary_WAS"
.Caption = "Submit Webins"
.Name = "BtnWAS" & myRow & i
.Font.Size = 10
End With
Next i
Application.ScreenUpdating = True
End Sub
Here's the code executed by the buttons:
Public Sub Create_Primary_IM()
MsgBox ("Row, Col of pressed button: " & ActiveSheet.Shapes(Application.Caller).TopLeftCell.Row _
& ", " & ActiveSheet.Shapes(Application.Caller).TopLeftCell.Column)
End Sub
Public Sub Create_Primary_WAS()
MsgBox ("Row, Col of pressed button: " & ActiveSheet.Shapes(Application.Caller).TopLeftCell.Row _
& ", " & ActiveSheet.Shapes(Application.Caller).TopLeftCell.Column)
End Sub

Here's what I believe to be the cause of the problem, its effect on the spreadsheet, and the solution.
The cause was duplicate button names. I had been naming the buttons based on their row and column at the time of creation. The problem is that rows and their buttons could be inserted or deleted anywhere in the sheet. It's not hard to see (except that I didn't see it for a while) that it was possible to insert a row and create its buttons in the same absolute row multiple times. This resulted in buttons with the same .name property.
The effect was that when I clicked one of the duplicate buttons it found the TopLeftCell.Row of the original button with that name, not of the row that the button was actually on. When I saved the file (not closed it) and clicked the same button, it had been renamed to something like 'Button nnn' and the correct row was returned. If I saved the file and re-opened it, the duplicate button name condition was still present. So Excel was replacing duplicate names in that collection when I saved the file, but not committing those changes when I closed the file.
The quickest solution was the easiest as well. I let Excel do the naming. I don't really need the buttons to have names. All I need is the row number. Another solution would be to generate a unique name, but what's the point if that's what Excel is going to do for me anyway?

Related

How do I prevent inserted images from clustering?

I have two sheets and one userform.
Sheet2 starts off completely empty. Sheet1 is completely empty except for one ActiveX command button that opens the userform when clicked.
The userform has one single label, that when clicked, will run some vba code that (1) inserts an image at a specified location in Sheet2 and then (2) inserts a row in Sheet2 above the image
My goal is to click on the userform's label X number of times such that I will end up with X number of images in sheet2 each separated one row apart from each other (my actual use case involves different images).
The problem I am facing is that when I first open the excel file (with sheet1 as the default sheet that appears upon opening) and start using my ActiveX command button as described, then when I navigate over to sheet2 to take a look, the images in sheet2 are all clustered together on top of each other.
Strangely enough, after I navigate over to sheet2 to take a look, then navigate back to sheet1 to continue using the ActiveX command button, then navigate back to sheet2 again to take another look, the subsequent images are now all neatly spaced out as intended.
Is there a way to make sure that right from the start, the images don't cluster when I first open my excel file and use the ActiveX command button?
Code contained in sheet1
Private Sub CommandButton1_Click()
UserForm1.Show
End Sub
Code contained in the userform (substitute your own pic filepath)
Private Sub Label1_Click()
Dim pic
Set pic = Sheet2.Shapes.AddPicture(Environ("USERPROFILE") & "\" _
& "Pictures\Account Pictures\monkey selfie.jpg", _
False, True, 1, 1, -1, -1)
pic.LockAspectRatio = msoTrue
pic.Placement = xlMoveAndSize
If pic.Height / pic.Width <= 14.3 / 47 Then
pic.Width = 40
ElseIf pic.Height / pic.Width > 14.3 / 47 Then
pic.Height = 10
End If
With pic
.Top = 20
.Left = 20
End With 'this position corresponds to cell A2
Sheet2.Range("A2") = "Filled" 'to indicate that the cell _
is supposed to contain an image
Sheet2.Rows(2).Insert Shift:=xlShiftDown 'both the image and _
the word "Filled" should be pushed down one row
End Sub
Thank you!

Excel - how to memorize click-state of a button workbook-wide? (e.g. to show-hide data or join with another button's state)

(TLDR: question is until "use-case:")
I am trying to erase values in an excel column when a button is pressed a second time.
At this stage, I have a button with following code that displays values from another sheet if pressed. The data is just displayed in the current sheet, it will never be more than a number of columns, say max 10, and they will be copied in / removed depending on button state.
(implemented as ActiveX button through developer tools):
Private Sub Button_Click()
Worksheets("myworksheet.xlsx").Columns("E").Copy Destination:=Sheets(1).Columns("S")
End Sub
I would like to store certain values globally for the duration of the session so if another button is pressed, it can get the state of this button too (in order to join data referred to by the two buttons).
What would be the best way to store these values and retrieve them?
PS this is my first foray into Excel vba. Main goal is minimum overhead but still extensible to more logic. So any ugly but quick solution that works up to say 20 params, is also fine with me... "time to market" is critical.
thank you very much for your time and help.
use-case:
Show all involved tables or objects in a data loading process, from input file to reporting domain - but only show those objects for which the relevant buttons are pressed. E.g. show all target tables in Domain "A", or show all reports using source files in soure feed "X".
button 1 has label "Domain A". If pressed, it will get all Jobs that are listed in the "Domain A" Column in another sheet and display them.
Button 2 has label "CORE Database". If pressed by itself, it will show all values from the "tables" column in the Database Sheet that have value "CORE" in another column.
If both buttons are pressed, I want to display all jobs in Domain A with all tables in "CORE Database" that they have as targets.
Then, if button "CORE Database" is pressed again, ie "toggled off", the display should go back to only showing all Jobs for Domain A.
Now if a third button is pressed, labeled "VIEW Layer", the combination of all views for all jobs in Domain A should show up.
Combining all three buttons should then show the combinaiton of all three.
The logic that is behind this, could be implemented in many was I guess, and I will see how far I get. The starting point is to have a decent location for storing these states, there may be up to 30 buttons if things get really wild.
Each button essentially adds its own column ("Dimension"), and whatever is displayed, will be a cross-section of all of these. So it is important, to know at any time, what buttons are pressed.
Update 2:
some example in data (warning, this goes beyond the original question):
Jobs sheet holds jobs and their tables.
Views sheet holds views and their source tables.
if only the "Jobs" button is selected, display:
SOURCE DATA: Jobs, deduplicated
if only the "View" button is pressed, display all the views in the database, deduplicated.
For example:
if only the "CORE" button is selected, display all the tables in the core database, for example:
JOINED DATA: tables, taken from the Jobs sheet and the view sheet, deduplicated
(edit: this is inconsistent, as the tables X and Y show up out of nowhere, they could be left out or more jobs showing)
if the Jobs and Core button is pressed, show (this is directly from the Jobs sheet):
SOURCE DATA: Jobs and their target tables
If the tables and views button is pressed:
(in this example case, View B is not delivered by any job)
SOURCE DATA: Views and their source tables
(so ViewA uses two tables as source)
if the "Job" and "View" buttons are pressed:
JOINED DATA: Views and relevant jobs, joined through their matching tables
and finally if all three buttons are pressed, essentially more columns are shown and more rows are shown, since there is less deduplication:
JOINED DATA: Views and relevant jobs, joined through their matching tables, showing all
(despite the confusing name "B", ViewB is not loaded by jobB. Should have picked different name, in retrospect)
This solution is based on a new worksheet with 3 activeX toggle buttons (all with default names).
I've first bound a cell to each toggle button - A1 to ToggleButton1, B1 to ToggleButton2 and C1 to ToggleButton3. This returns either TRUE or FALSE to the cell based on the state of the ToggleButton.
ToggleButton control MS documentation
For visuals, here is the source data:
Note: As it's not that easy to see whcih ones are clicked in at a glance, you can change the back colour of the buttons in the ToggleButton_Click event.
Like so:
With Sheet1.ToggleButton1
If .Value = True Then
.BackColor = &HFF00&
ElseIf .Value = False Then
.BackColor = &H8000000F
End If
End With
End Sub
Private Sub ToggleButton2_Click()
With Sheet1.ToggleButton2
If .Value = True Then
.BackColor = &HFF00&
ElseIf .Value = False Then
.BackColor = &H8000000F
End If
End With
End Sub
Private Sub ToggleButton3_Click()
With Sheet1.ToggleButton3
If .Value = True Then
.BackColor = &HFF00&
ElseIf .Value = False Then
.BackColor = &H8000000F
End If
End With
End Sub
I first define the last column dynamically based on the first row of values.
Info about finding the last row/column
Note: I've included 2 statements for SourceLastCol - use one or the other depending on how your sheet is set up - If you leave it as is, it will use the value from the 2nd statement.
Using the last column found, I then loop each cell in the range for row 1 from column A to the last column.
If the value is True it then spits that columns values out into another worksheet (in this case, Sheet2) to TempDestinationRange using TempArray. (See http://www.cpearson.com/Excel/ArraysAndRanges.aspx for more info about this method).
It would probably be best to have a Submit command button to trigger the sub rather than run it each time a toggle button is clicked (as if you have 30 and someone changes them all that's a lot of redundant worksheet changes).
Sheet2 output :
Sub CheckToggleAndJoinData()
Dim SourceLastCol As Long
Dim SourceLastRow As Long
Dim SourceRange As Range
Dim CellToCheck As Range
Dim TrueRange As Range
Dim DestinationRange As Range
Dim DestinationLastRow As Long
Dim DestinationLastCol As Long
Dim TempArray As Variant
With Sheet1
SourceLastRow = .Cells(Rows.Count, "S").End(xlUp).Row
.Range("S1:S" & SourceLastRow).ClearContents
SourceLastRow = 0
SourceLastCol = Sheet1.Cells(1, Columns.Count).End(xlToLeft).Column 'Use this one if there is no data to the right of your source columns.
SourceLastCol = Sheet1.Cells(1, 1).End(xlToRight).Column 'Use this if there is data to the right of your source columns (note this will not work if there are blank gaps in your source columns)
Set SourceRange = .Range(.Cells(1, 1), .Cells(1, SourceLastCol))
End With
Dim TrueColumnArray As Variant
Dim ColumnToCheck As Long
Dim ColumnCounter As Long
ColumnCounter = 0
For ColumnToCheck = 1 To SourceLastCol
With Sheet1
If .Cells(1, ColumnToCheck).Value = True Then
ColumnCounter = ColumnCounter + 1
SourceLastRow = .Cells(Rows.Count, ColumnToCheck).End(xlUp).Row
Set TrueRange = .Range(.Cells(2, ColumnToCheck), .Cells(SourceLastRow, ColumnToCheck))
DestinationLastCol = Sheet2.Cells(1, Columns.Count).End(xlToLeft).Column
If ColumnCounter = 1 Then
Set DestinationRange = Sheet2.Cells(1, DestinationLastCol)
Else
Set DestinationRange = Sheet2.Cells(1, DestinationLastCol + 1)
End If
TempArray = TrueRange
DestinationRange.Resize(UBound(TempArray, 1), 1).Value = TempArray
End If
End With
Next ColumnToCheck
End Sub
To de-duplicate your data (as mentioned in some comments/edits/chat) you may want to look into using a Dictionary object as this will store each input value as a Key - ignoring subsequent occurrences of this value.
-edit: I wrote this BEFORE Samuel's answer, so that is obviously making this irrelevant :)
for now I will just go with following solution, it works for me, beauty is not so important right now
-update: I am now going with global variables, of course my programmermind tells me this is risky and dangerous, but I just need results for now, and they are booleans for states, only. Any suggestions are still very welcome.
Here is some code that I have now:
Public CORE_Pressed As Boolean
Sub clearCol(colLabel As String)
Columns(colLabel).ClearFormats
Columns(colLabel).ClearComments
Columns(colLabel).ClearHyperlinks
Columns(colLabel).Clear
End Sub
Private Sub Workbook_Open()
SLJM_Pressed = False
End Sub
Private Sub CORE_Button_Click()
CORE_Pressed = Not CORE_Pressed
If CORE_Pressed = True Then
Worksheets("CORE.xlsx").Columns("E").Copy Destination:=Sheets(1).Columns("S")
Else
clearCol ("S")
End If
End Sub
I will probably move the button handling into a central function, but this is just to show the current approach.
Also many thanks for the comments posted by Samuel Everson, they gave a very interesting alternative for when things need to be more "professional". global variables are ugly I know.

Visual Basic Excel create Checkbox in Code

I've added a button to an Excel file which, when clicked, reads a text file and populates a column with lines from a text file. I need to add a checkbox to cells adjacent to certain lines, depending on what the line contains.
Can I create components like checkboxes in code, and if so, how?
Any responses are appreciated.
While the link #Siva provided is certainly valid I just prefer to have an answer on StackOverflow instead of an external link. Hence, here is the solution you might be looking for:
Option Explicit
Public Sub tmpSO()
Dim i As Long
Dim chk As CheckBox
With ThisWorkbook.Worksheets(1)
.CheckBoxes.Delete
For i = 1 To .Cells(.Rows.Count, "A").End(xlUp).Row
If .Cells(i, "A").Value2 = "need checkbox" Then
Set chk = .CheckBoxes.Add(Left:=.Cells(i, "B").Left, Top:=.Cells(i, "B").Top, Width:=.Cells(i, "B").Width, Height:=10)
chk.OnAction = "runThisSub"
chk.Name = "CheckBowInRow" & i
chk.Caption = "CheckBowInRow" & i
End If
Next i
End With
End Sub
Sub runThisSub()
MsgBox "You clicked the checkbox " & Application.Caller _
& Chr(10) & "in cell " & ThisWorkbook.Worksheets(1).CheckBoxes(Application.Caller).TopLeftCell.Address
End Sub
Copy both subs into a Module into your Excel file and change in the first sub
the sheet where the text is imported to (here it is Worksheet(1)),
the column where the condition can be found (here column A), and also
what the condition is (here the value in column A must be need checkbox).
The code will now look through all cells in column A in sheet Worksheet(1) and check if the value is need checkbox. If so, the code will automatically add a checkbox into column B adjacent to that cell.
If you click on any of the newly created checkboxes then the second sub fires up and will show you in a message box which checkbox in which row has been clicked.

Is There A Fast Way To Add Multiple Text boxes With Scrollbars?

Because the cells in my project contain so much data I have had to insert textboxes that have scrollbars to see all the data (they are linked to the cell which sit behind them on the spreadsheet). Is there any fast way to do the same thing on a column of 1000 records or will I have to go through manually and link the textbox to the specific cell? Is there a faster way?
Also If an issue comes in that is a reply to the original issue I need it to use the original ID (I have used auto IDS, which can be seen in the spreadsheet). Any recommendations?
Slowly I am getting better at excel and VBA but I need a hand sometimes ^_^
I have attached the spreadsheet which contains an example of 2 records I made. The final sheet will have 1000 records. (Please download the spreadsheet and open in excel)
LINK To Spreadsheet
A few things:
You should change the cell formatting to "Top Align" the text in the cells. This will cause the cell to show the first line of the long text in the Query cells.
Instead of using the "send email" text in a cell why not add a single button to email the currently selected row. (use insert on the ribbon in the developer tab (you have to change the excel options to show the developer tab).
The code to send an email might be better if it updated a new column with the date it was sent, and in the event that it has already been sent, it could prompt the user to confirm.
if not isempty(cells(r, ColNumberWithSentdate) ) then
if vbno = msgbox ("Are you sure you want to send the email again?", VbYesno) then
Exit sub
end if
end if
All the textboxes you have added are really slowing down the spreadsheet.
why not just have one tall row at the top above the table with the filters. The tall row would show the data from the currently selected row in the table. Your table rows could then probably be less high.
Add a single text box.
Use ALT+click and drag to resize text boxes to fit cell exactly.
Change or view the name of the textbox in the named range area to "TextBoxQuery".
Add code to change the text in the summary row
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
' Say the tall row is in row 2
If Target.Row <= 2 Then
Exit Sub
End If
Dim i As Integer
For i = 1 To 8
Cells(2, i) = Cells(Target.Row, i)
Next i
End Sub
You could even allow the user to edit the text in the tall row and add a button to save the changes they entered:
A. Add an ACTIVEX button in the summary row labelled "SAVE"
(Then you can edit the vba in the sheets module for the button)
B. Add a cell somewhere that records which row is being displayed in the summary row.
C. When the save button is clicked, write code that copies all the values in row to back to the row recorded.
NOTE that if the user deletes a row in the table or sorts the data in the table the row stored will be wrong. So before copying the data, you might like to check to see whether the row has moved. ie check a KEY value (ie ones that never changes) is the saem in both rows.
Private Sub CommandButton1_Click()
Dim i As Integer
For i = 1 To 8
Cells(Cells(1, 1).Value, i) = Cells(2, i)
Next i
End Sub
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
' Say the tall row is in row 2
If Target.Row <= 2 Then
Exit Sub
End If
' Cell A1 is used to store which row is displayed
Cells(1, 1) = Target.Row
Dim i As Integer
For i = 1 To 8
Cells(2, i) = Cells(Target.Row, i)
Next i
End Sub

Excel VBA: getting row of clicked button [duplicate]

This question already has answers here:
Get corresponding Range for Button interface object
(3 answers)
Closed 3 years ago.
I am trying to make a button in Excel which copies a certain range of cells from the active workbook to another workbook. The copying of this range works perfectly when I specify a fixed range, but I am stumped on how to figure out the row of the clicked button.
Every row contains 7 or so cells, and the 8th cell contains a shape with a macro attached to it (the button).
When the user presses this button the 7 cells on the same row as the row containing the pressed button need to be copied.
Using ActiveCell is of no use, since pressing the button doesn't actually set that cell as active. I searched around a lot but I can't seem to find how to obtain this value.
Once I have the row number of the clicked button I can figure the rest out on my own.
Each Shape has TopLeftCell property. It contains a cell within which the top left corner of the shape resides.
Try this:
Sub Mainscoresheet()
' Mainlineup Macro
Dim b As Object, cs As Integer
Set b = ActiveSheet.Buttons(Application.Caller)
With b.TopLeftCell
cs = .Column
End With
MsgBox "Column Number " & cs
End Sub
Excellent answer. Btw It also works for Rownumber!
'Same for rownumbers!
Sub Mainscoresheet()
' Mainlineup Macro
Dim b As Object, RowNumber As Integer
Set b = ActiveSheet.Buttons(Application.Caller)
With b.TopLeftCell
RowNumber = .Row
End With
MsgBox "Row Number " & RowNumber
End Sub
This works too! Selects the cell that the generated button resides in (knew it was in column "K" but that could be calculated too!.
ActiveSheet.Range("K" & ActiveSheet.Buttons(Application.Caller).TopLeftCell.Row).Select

Resources