VBA: focus of selection after SelectAll - excel

I have a VBA code that opens a workbook. It then does the following:
ThisWorkbook.Sheets("Sheet1").Shapes.SelectAll
Selection.Delete
I thought after the SelectAll, the Selection should be the shapes just being selected, which are what I want to delete. No. It's not. It's the active cell in the just-opened workbook and it's that active cell that's deleted. Usually, Selection after Select refers to the objects just being selected. Apparently, in the case of SelectAll, that's not true. How to set the focus to the just-selected shapes?
I changed my approach by doing SelectAll then assigned the selected shapes to a shaperange object following the method in Microsoft Docs. The method is in the following:
Set myDocument = Worksheets(1)
myDocument.Shapes.SelectAll
Set sr = Selection.ShapeRange
My code is:
ThisWorkbook.Sheets("Sheet1").Shapes.SelectAll
Set sr = Selection.ShapeRange
Unfortunately, that document doesn't say what the sr is declared to. I have declared it as a shape, shapes, ShapeRange, Object, Variant. In every case, Excel complained "Object doesn't support this property or method." I wonder what it's set to in the MS document.
I know I can delete all shapes using a For loop but I'm trying to avoid doing a loop.

ThisWorkbook.Sheets("Sheet1").Shapes.SelectAll
Selection.Delete
is fundamentally wrong because it attempts to convert a Shapes Collection into a Selection object.
The Selection object exists entirely and almost exclusively for the use and benefit of the user: the user may select something to show to VBA or VBA may select something to show to the user. In your example the user is excluded from the process. Therefore involvement of the Selection object is a waste of time and resources. You wouldn't have to ask your question if you weren't trying to make something work that isn't designed to work in that way.
The correct approach is to identify the object and do with it whatever you want. The object is either the collection of all shapes on the worksheet (ThisWorkbook.Sheets("Sheet1").Shapes) or any member of it, such as ThisWorkbook.Sheets("Sheet1").Shapes(1). You also have ThisWorkbook.Sheets("Sheet1").Shapes.Count at your disposal to loop through them all or For Each Shp in ThisWorkbook.Sheets("Sheet1").Shapes.
The ShapeRange object contains another collection. It's name promises a smaller collection than all shapes on the sheet. Don't let the choice of collections confuse you. The job is to identify one or several shapes and delete them. The fact of the matter is that you do have to deal with a member of a collection and not the Selection object.

Related

All properties of Application.ActiveSheet in Excel - Object Browser does not list the properties of Members

Microsoft Docs do not list all the properties of Object Application.ActiveSheet:
https://learn.microsoft.com/en-us/office/vba/api/excel.application.activesheet
It has only included some of the properties like: ActiveSheet.Name. But from VBA scripts I found on the internet, I know that there are more properties such as: ActiveSheet.Type
So I thought maybe I can list them all with this VBA code: from: VBA collection: list of keys
Sub AktivSheet()
Dim key As Variant
For Each key In Application.ActiveSheet
Debug.Print "Key: " & key, "Value: " & Application.ActiveSheet.Item(key)
Next
End Sub
But it didn't work, and Microsoft Docs suggests to use Object Browser. But Object Browser does not list the properties for Application.ActiveSheet:
I'm out of ideas! I just want a list of all ActiveSheet properties. Because I simply need to know the Text Direction of the Active Sheet, as in if it's Left to Right OR Right to Left.
I just want a list of all ActiveSheet properties
You cannot easily get that programmatically with 100% VBA code, VBA has pretty much zero reflection capabilities.
Now, assuming we're not looking for a programmatic way to retrieve object properties, here's how to use the object browser to get what you're looking for.
First, right-click anywhere in the object browser and select the "Show hidden members" option to reveal the full breadth of the libraries you're looking at. This affects the names list dropdown when editing code: you'll be shown hidden members now.
One of the hidden modules in the Excel type library, is a module named Global, with a hidden _Global interface:
That hidden global module is how you can type MsgBox ActiveSheet.Name and it "just works" (assuming there is an ActiveSheet - it could always blow up with error 91 when there's no active workbook open in the Application instance you're working with) even though you haven't specified what Workbook you're working with: implicitly, ActiveSheet is just working off whatever the ActiveWorkbook is.
So ActiveSheet is a property, not an object. It's a property that returns an object, but its declared type is Object.
This means any member call you make directly against ActiveSheet, is implicitly late-bound: you can type MsgBox ActiveSheet.Naem and VBA will happily compile the typo (Option Explicit can't save you here), and only blow up at run-time with error 438 "I can't find that property!".
In order to know what properties the ActiveSheet has, we need to know what run-time type we're looking at. And since a sheet in a Workbook object can be a Worksheet, a Chart, or several other types of legacy "sheet" objects, there is indeed no member accessible at compile-time, because at compile-time the ActiveSheet is just a pointer to an Object, and what type of object that is will only be known at run-time.
So instead of coding against ActiveSheet, we code against Worksheet, because we know the particular sheet we're expecting to work with is a Worksheet object.
Dim Sheet As Worksheet
Set Sheet = ActiveSheet
Now when we type Sheet., we're early-bound (types involved are known and resolved at compile-time) and helpfully provided with a list of all available members:
Every time you access a member (function, property) that returns an Object or a Variant, any member call made against it will be late-bound.
Strive to stay in the early-bound realm: declare local variables as needed, such that the compiler gets to "see" and validate everything! Try typing the below code to feel the difference - whenever you type a . dot and nothing comes up, it's a sign the compiler is losing sight of what's going on and you're possibly moving compile-time errors to run-time:
MsgBox ActiveSheet.DisplayRightToLeft '<~ late bound
Dim Sheet As Worksheet
Set Sheet = ActiveSheet
MsgBox Sheet.DisplayRightToLeft '<~ early bound

Setting linkedcell of multi-option field

I have an Excel sheet with quite a few groups of option buttons (inserted from form controls and grouped using group boces). What is the easiest way to set the linked cell of each group using VBA code? I tried
ActiveSheet.Shapes.Range("test").Select
.LinkedCell = Range("A1")
but it had no effect. "test" is the name I set for one of the option buttons in the corresponding group.
(The reason for trying to set the LinkedCell via VBA is that these links are sometimes lost for reasons I don't yet understand. If anyone can point out a possible scenario how this could possibly happen in the first place, I would be very grateful.)
Using a named range will only point to one or more cells on a worksheet. It does not refer to a form control. You're likely using a Form Control Option Button somewhere on your worksheet. The code to set the LinkedCell property is
Dim optButton As Shape
Set optButton = Sheet2.Shapes("Option Button 2")
optButton.ControlFormat.LinkedCell = "H4"

Paste/Copy Range of Cells from Excel to a Bookmark in Word using WORD VBA

I am looking at inserting/pasting a range of text data (40 columns) from Excel into bookmarks in Word. Most of the answers are done using Excel VBA, which is so not practical for my use case as I will have the Word document open, add a button that would run this 'import data' macro. I actually already have a button in the doc that inserts images into bookmarks, so that's one more reason I don't want to do it via Excel VBA.
I know this is not great code, but for the lack of definite leads, I'm throwing it here and hope that this gives you an idea of what I'm trying to achieve:
Sub ImportData()
Workbooks.Open ("\Book2.xlsm")
ActiveWindow.WindowState = xlMinimized
ThisWorkbook.Activate
Windows("Book2.xlsm").Activate
Range("A1:AF1").Select
Selection.Copy
Documents("test.docm").Activate
Selection.GoTo What:=wdGoToBookmark, Name:="Overlay_1"
Selection.Paste
End Sub
PS: It would be great if I could sort of 'transpose' the 40 columns into rows as it is pasted in Word.
Here's an update to my code based off #Variatus 's advice:
Sub ImportData()
Dim wb As Workbooks
Dim ws As Worksheets
Dim objSheet As Object
Dim objWord As Object
Set objWord = CreateObject("Word.Application")
wb.Open ("C:\Users\pc\Documents\Book2.xlsm")
Set objSheet = CreateObject("Excel.Application")
ActiveWindow.WindowState = xlMinimized
Set ws = Workbooks("Book2.xlsm").Sheets("Sheet1")
ws.Range("A1").Value.Copy
With objWord.ActiveDocument
.Bookmarks("Bookmark_1").Range.Text = ws.Range("A1").Value
End With
End Sub
I'm getting this error:
Runtime Error '91':
Object variable or With block variable not set.
Notice how I stuck with a single cell reference for now (A1). I'll just update my code as I learn along the way :)
When you click the button in your Word document you want the following sequence to be initiated.
Create an Excel application object. Make sure that a reference to Excel has been set (VBE > Tools > References) so that Excel's VBA objects are available.
Using the Excel application object, open the workbook. Create an object. Place the object in an invisible window.
Definitely forget about activating or selecting anything in either the workbook or your Word document. The latter is active and remains active from beginning to end. The bookmarks are points in your document you can reference and manipulate by name without selecting them. The Excel workbook is invisible. You can access any part of it using the Range object.
The data you want from your workbook are contained in Worksheets. Be sure to create an object for the worksheet you are about to draw data from.
Excel tables don't translate very well into Word tables. If you do want to go that way I suggest that you use VBA to create the table you want in Excel (transpose the data before you import them into Word). However, you may find it easier to first create the tables you want in Word and then just copy values from your Excel source into the word tables. That would involve taking one cell value at a time and placing it into one Word table cell. Transposing would be done by the algorithm you employ.
Close the workbook. Quit the Excel application. Set the Excel application = Nothing. At the end of your macro everything is as it was before except that your document has data in it which it didn't have before.
Each of the above six points will lead you to at least one question which you can ask here after you have googled the subject and written some code. In fact, I strongly urge you to create one Main procedure (the one which responds to your button click) and let that procedure call various subs which carry out the individual tasks and functions to support the subs. The smaller the parts you create the easier it is to write the code, to find questions to ask and get answers to them. If you plan your project well expect to have about 12 procedures in it by the time you are done. Good luck!

Calling a custom built VBA function when an Excel cell is changed

I'd like to preface this question by saying that I am an undergrad in college who knows C++ and has a very rudimentary understanding of VBA.
Now then, as stated in the title I need some help configuring some VBA code for an Excel worksheet so that whenever a cell in a column (specifically the D column) is modified it will automatically update other cells within the same row.
Essentially I want this to work such that when user Bob modifies cell D26 (for example) it will call a custom function I built and insert that code into cell B26 and then repeat with a different function for cell C26.
However, this function needs to be such that if cell D27 is modified it will only modify other cells in row 27, leaving row 26 and prior or subsequent rows alone until such a time as this function is called in D28 and so on.
I'm not entirely sure if this is even possible but I'd be gracious if anybody could help me configure this.
The code I built/scavenged from the internet for my custom function is this:
http://pastebin.com/RE0V2nrT
The second function I want to call for this project is the =TODAY() function built into Excel.
The code I have scraped together so far for checking if the cell has changed is this:
http://pastebin.com/S5E8cmty
If anybody could help me understand how to write what I'm looking for it would be much appreciated. If you have a different approach to solving the issue I would also love to hear it... as long as you could help me then enact your solution, haha!
Anyways, thanks to anybody who replies.
Have a look at the worksheet events available within the Excel namespace.
For this, you would use the Change event
If you double click on the worksheet you want to monitor, you can insert a Worksheet_Change sub. Then you can use the intersect function to check if the changed cell was within your range you want to monitor (e.g. D:D).
You can specify which cells you want to change. Here I just gave an example based on what you asked. This will put the output of your function into cell B[R] and put the current date into cell C[R]. Note that I'm using the Now() function since there is no Today() function in VBA. Since this returns both date and time, I'm using the Format function to get just the date.
Just for fun, let's go a little further into the object model and first get the Worksheet object to which the target range belongs. This is not 100% necessary - you could just rely on ActiveSheet. Now, you probably don't need to do this, and it's mostly just for fun, but it's also worth noting that if you were programmatically making changes to this sheet, but had not activated this sheet first (so another sheet was active) and you had not turned off EnableEvents you would get some strange results :)
Private Sub Worksheet_Change(ByVal Target As Range)
Dim TargetSheet As Worksheet
Set TargetSheet = Target.Parent
With TargetSheet
If Not Application.Intersect(Target, .Range("D:D")) Is Nothing Then
.Cells(Target.Row, 2) = ExtractWindowsUser()
.Cells(Target.Row, 4) = Format(Now(), "YYYY-MM-DD")
End If
End With
End Sub
Explanation
Worksheet change sub is declared like this. The Worksheet objects have pre-defined method stubs for events. Kind of like an interface, though not listed as an interface in the documentation. If you think of it in that concept, this is your event handshake. See the link I posted above for a list of the worksheet events available.
Private Sub Worksheet_Change(ByVal Target As Range)
In the next lines we are getting the worksheet object to which the object named Target belongs. You can see in the sub declaration that Target is declared as an object of the type Range. If you check out the Worksheet object (linked above) or the Range object documentation you'll see that the range object is a member of the worksheet object, and the documentation kind of sucks here, but FYI the worksheet object is contained within the Parent property. Now, originally I had my code using the ActiveSheet member of the Application object - but I've edited it for the reasons given in my answer above.
Dim TargetSheet As Worksheet
Set TargetSheet = Target.Parent
I use With Blocks to save typing the same Worksheet reference in multiple places. A With block just lets me access the members of the namespace specified (in this case members of the object TargetSheet) by typing .SomeMember. The compiler understands that every reference like this refers to whatever is specified in the opening With .... statement. I personally like this for readability, but I also recommend it for maintenance (change reference one place vs many). Also having a single reference gives a tiny, insignificant, probably not worth mentioning performance boost over multiple references as well.
With TargetSheet
Next we check whether or not Target is within the range of cells we want to watch. The If....Then should look familiar enough. For our condition we use the boolean operator Not to check if the result of the intersect function (linked above) Is Nothing. The reason we do this is to check if the return is allocated. If an object is allocated the Not SomeObject Is Nothing condition will evaluate to False. If the object is not allocated (i.e. our Intersect function failed to return anything) then the statement evaluates to True. So, from the Intersect function documentation we know that if our return is allocated, the ranges intersect and the intersecting range object was returned. Thus if we want to know if they intersect, we can just check for the opposite of a failure.
If Not Application.Intersect(Target, .Range("D:D")) Is Nothing Then
The next lines then just execute some code on cells within the same row as Target. We use the Cells member of the worksheet object to specify what cells to modify. Per the documentation, the default property for Cells is Item which lets us access a range object through a row and column index like this: .Cells[Row,Column]. So, I simply use the row of our Target object and the column you wanted (column "A" =1, "B"=2, etc. You can see this by changing excel properties to R1C1 reference style if you are interested).
.Cells(Target.Row, 2) = ExtractWindowsUser()
And I think the Format() and Now() functions are pretty well explained in the documentation.

Protect shapes so users can modify but not delete

I have a workbook with some shapes that I don't want users to delete easily. They are linked with an index, and the index number is linked with a whole other bunch of stuff.
However, the user needs to move the shapes around and resize them.
How can I set the protection on the sheet to allow users to modify the shapes but not delete them?
The ability to do this in VBA would be useful too, as a few of my macros unprotect and then protect cells.
This is not possible.
You can lock all shapes in the workbook by using this code
Private Sub Workbook_Open()
ActiveSheet.Protect Password:="test", userinterfaceonly:=True
Dim shape As shape
For Each shape In ActiveSheet.Shapes
shape.Locked = True
Next
End Sub
or you build a plugin for Excel in dot.net.
This blog describes how you would do that for shapes and there you can use events to overide the delete.
create excel events for shapes
or
Microsoft excel shape events
This is indeed possible but with a slight workaround. No windows API. However, what are your shapes?
You can utilize ActiveX image controls and manipulate the MouseDown/MouseUp events to detect starting position and ending position via the left and top properties. Once calculating the difference in positions, you can set the image's new top/left properties to the appropriate numbers. You can even look into possible drag effects for the image so the cursor does not appearing totally still during a drag.
Alternatively, you can turn on that sheet's protection for editing objects which prevents moving/deleting innately. It still allows clicking, at which point you can assign a macro that reacts somewhat similarly to solution #1 but is more complex. Your macro can read the picture name by referencing the "application.caller" property. Each picture name can have different procedures associated with it. Read the current top and left properties of the shape. Continuing breaking the automation down into 2 steps. Programmatically unprotect the sheet, and monitor with a public boolean that an operation is in progress. Use the selection change event to monitor the next cell click at which point you can capture the subsequent top and left properties, eventually calculating where the shape will move. Programmatically protect worksheet again.

Resources