I have a particular word in my document that has various inline formatting from previous authors. I want to find that word and strip out its inline formatting. If I were using the keyboard, I would select the items one at a time, applying CTRL+Spacebar to each. Instead, I wrote a macro. In general I don't want to assume the nature of the inline formatting; I don't want to worry if it's bold or italics or anything else; I just want to execute a generic CTRL+Spacebar, which translates to .Font.Reset in VBA. This doesn't work, though. Here's my code, which does work, but please see the comments I added, which collectively ask why .Font.Reset doesn't work:
Sub Inline_Formatting_Replacement()
Dim MyDocRange As Range
Set MyDocRange = ActiveDocument.Range
With MyDocRange.Find
.ClearFormatting
.Text = "word_with_inline_formatting"
With .Replacement
.ClearFormatting
.Text = "word_with_inline_formatting"
.Font.Bold = False 'I shouldn't have to use this.
.Font.Italic = False 'I shouldn't have to use this.
.Font.Reset 'This should work instead of using .Bold and .Italic, _
'but this line does nothing. Why?
End With
.Forward = True
.Wrap = wdFindStop
.Execute Replace:=wdReplaceAll
End With
End Sub
As Cindy Meister pointed out, Font.Reset is a method, not a property.
You're configuring Range.Find.Replacement.Font, telling the object model what you want to replace and how it should be formatted. That works when you invoke read/write properties, because read/write properties store state. Methods act on that state.
It makes more sense when you understand how properties work - here's an example:
Option Explicit
Private internalState As Boolean
Public Property Get State() As Boolean
'invoked when .State is on the right-hand side of an expression, e.g. foo = obj.State
State = internalState
End Property
Public Property Let State(ByVal value As Boolean)
'invoked when .State is on the left-hand side of an expression, e.g. obj.State = foo
'the "value" parameter is the result of the right-hand side expression.
internalState = value
End Property
Imagine this internalState contains the information about whether the font should be bold or not: the Font property on the object returned by Replacement is a Font object, i.e. an instance of the Font class. So when you do .Bold = False you're encapsulating that value inside the internal state of that Font object.
Now say you also have a method in that imaginary class module:
Public Sub Reset()
internalState = False
End Sub
Now I don't know what Reset really does - one would have to peek at the source code for that... or see if the documentation has anything useful:
Removes manual character formatting (formatting not applied using a style). For example, if you manually format a word as bold and the underlying style is plain text (not bold), the Reset method removes the bold format.
https://msdn.microsoft.com/en-us/vba/word-vba/articles/font-reset-method-word
So why doesn't it reset the Font of the Replacement object? It's actually a very good question: one could reasonably expect that invoking Reset on the Replacement.Font would effectively "reset" that font.
I'm not very familiar with the ms-word object model (Cindy is a pro though!), but in order for the .Reset to work as a replacement, I believe the Font class would need to do something like this:
Option Explicit
Private doReset As Boolean
Public Property Get ShouldReset() As Boolean
ShouldReset = doReset
End Property
Public Sub Reset()
'does whatever it does
'...
doReset = True
End Sub
And then when the replacement is actually carried out, Word can inspect ShouldReset and invoke the method on the Font instance of the affected Range - which is a different instance than the Font instance on the Replacement object.
Except it wouldn't make any sense for a Font to do something like above, because of course if you invoke Reset then it should reset - what's missing is something like a ReplacementFont class that could reasonably hold that state/information... and it doesn't exist.
If VBA had functions as first-class citizens, then you could perhaps do something like this bastardized nonsensical pseudo-code:
Set .ReplacementAction = New Function(ByVal r As Range)
r.ClearFormatting
r.Text = "word_with_inline_formatting"
r.Font.Reset
End Function
And then when you'd go Find.Execute, the hypothetical ReplacementAction function would pass the actual Range as a parameter to this ...delegated action, and .Reset would operate against the target Range.Font when it's invoked, as opposed to operating on the Replacement.Font when it's setup.
Alas, VBA can't do delegate stuff... or can it?.
I'm pretty sure that calling Find.Replacement.Font.Reset just clears out any replacement font information, so when you do the replace, the font is not changed.
I'm running into a similar effect, where I want to remove character styling from text globally using find/replace and I haven't found any way to do that.
Replacement.Font, .Style, etc seem to have no way of setting them to "clear out any specific font/style/etc". The only choices are to do nothing or set specific values.
As a result in my case I'll have to write a loop that finds each search hit individually and does the equivalent to ctrl-space on the resulting range. This unfortunately would be much slower than the global replace.
Related
I have a VBA code where I need to define a constant string containing Non-Unicode characters (£). As some might know, VBA Editor doesn't support non-unicode and uses windows "System Locale" setting in regional and language setting to parse/map these characters. The machine I develop the code is set to English system Locale but some of the users have that setting as other languages, e.g. Chinese which turns the string constant to question mark (£ --> ?).
Now, £ = chr(163), however, you cannot use chr as part of defining a constant in VBA. So while this is allowed:
public const mystring = "reference constant string with £"
this is not allowed in VBA"
public const mystring = "reference constant string with " & chr(163).
One way around is to define mystring as a public/global variable:
Constants.bas
public mystring as string
and then set the public variable on start of running code or Excel opening.
ThisWorkbook
Private Sub Workbook_Open()
mystring = "reference constant string with " & chr(163).
End Sub
One issue with this process is the public variables get cleared when error happens or the code stops. To keep the value an alternate I came across was to avoid public variable and instead use a public property get. Note that I have to include this a part of a class.
**.cls
Public Property Get mystring () As String
mystring = "\R;;" & Chr(163)
End Property
So, now I am wondering if there will be any issue with this approach? or perhaps there is a better approach to address the constant variable with non-unicode character.
The main issue is the name of the module, Constants - it's misleading, since neither a public/global variable nor a public get-only property are constants.
Side node, constants not being assignable to non-constant expressions isn't a limitation that's specific to VBA.
Properties are perfectly legal in standard modules, and public get-only properties are a perfectly fine way to expose a read-only value that needs to be constructed at run-time.
the public variables get cleared when error happens or the code stops
Assuming "when error happens" involves clicking End and effectively ending the execution context, that is true of everything exposed anywhere, whether it's a global variable, a public property, an object, or anything that exists in-memory at run-time... and that's just normal stuff - the value is simply available on-demand any time.
Again there's no need to have a class module for this, this is perfectly legal in a standard module:
Option Explicit
Public Property Get MyString() As String
MyString = "\R;;" & Chr(163)
End Property
If re-creating the string every time the property getter is accessed is a problem, then you need a way to persist its value in a backing field - but then, this backing field (whether it's in a class or standard module) only has a value when the execution context exists, meaning it has exactly the same problem a global variable would have: execution stops, it's gone.
One work-around could be to use a class with the VB_PredeclaredId attribute set to True (default is False).
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "Class1"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = True
Option Explicit
Private internalString As String
Private Sub Class_Initialize()
internalString = "\R;;" & Chr(163)
End Sub
Public Property Get MyString() As String
MyString = internalString
End Property
And now VBA will automatically create an instance of Class1 whenever it's referenced, as soon as it's referenced, and that instance remains "alive" until an End statement is explicitly executed, or the execution context is otherwise terminated. Exactly like, say, a UserForm class, you access this default instance using the class name as an identifier:
Debug.Print Class1.MyString
If the default instance existed when Class1 was referenced, the internalString is returned. If it didn't, then Class_Initialize executes, then the internalString is returned.
I would like to make a class which looks like a String to VBA. That is, if I make a function with a string argument, and pass this object instead, I won't get any errors.
I thought Implements String would work, but apparently it's not even an option!
Why is it not possible, and is there any way to get the behaviour I'm after? (Of course I could just make my own IString interface and specify my functions to request that, but I don't want to do that)
I'm trying to make a file-selection dialogue, which I can pass to any functions that require string-filepaths as arguments. This would be a neat self contained way of retro-fitting file-selection to existing functions.
Because the String is not an "object" to VBA, as it is to other languages like Java or .NET. If you want custom behavior, I'd probably just create a custom VBA class that wraps a string, rather than implementing it, and return a String output, in similar vein to a string builder class.
With credit to the awesome Chip Pearson (http://www.cpearson.com/excel/DefaultMember.aspx):
You can do this by exporting the .cls to a text file; editing it Notepad to add a default attribute; saving it; then re-importing it into VBA. Let's give you an example.
In a class module:
Property Get Value() As String
Value = "Hello"
End Property
Then export the module. I called it Str.cls. I then opened this file in Notepad, and added the following line (as marked):
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "Str"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Property Get Value() As String
Attribute Value.VB_UserMemId = 0 ' <-- THIS is the line I added.
Value = "Hello"
End Property
Save the file again in Notepad: then in the VBE, re-import it. Note however that the extra line (and all the other surrounding information) will NOT appear in the VBE, so it will look like your code hasn't changed at all. However you can now do the following (in a regular module):
Sub ReturnSringClass()
Dim S As New Str
MsgBox S
End Sub
Note now no property is required to be named with "S". It just behaves exactly like a string. You should be able to put a file selector in your class like this.
I have a Userform that has a Checkbox. I am able to check the value of it, but it is always False whether it is checked or not.
Update
This is how the UserForm is being called (This is in another UserForm ):
Private Sub AddOutgoingbtn_Click()
With New AddIncomingForm
.TopBottom.Value = False
.Show
.Repaint
End With
End Sub
End Update
I created a sub to look for a change in the value like:
Sub TopBottom2_Change()
With AddOutgoingForm
If .TopBottom2.Value = True Then TopBottom = True
If .TopBottom2.Value = False Then TopBottom = False
End With
End Sub
But no matter what I do the .TopBottom2.Value is always False.
I've put a breakpoint on the With line so that I know it is hitting this Sub, then I step through it each time. I open the UserForm and check the box, step through and the value is False, then I uncheck the box and step through. The value is still False.
I am not setting the value in any other way with VBA. I am checking the value in the UserForms code, not in any other place.
I have an If in a Calculation Module that is looking at this value for when it is true or falses, but it is always false.
Here are all the Properties of the Checkbox:
With AddOutgoingForm
That's referring to the form's default instance, which may or may not be the instance that's currently being displayed.
You have two options:
At the call site, instead of doing this (or something similar):
With New AddOutgoingForm
.Show
'...
End With
Do this:
AddOutgoingForm.Show
'...
That way you'll be working with the default instance and the checkbox value-check should work.
...but IMO that's a very very bad idea, because then your form contains code that will only ever work when you're showing the default instance.
Leave the call site alone, and NEVER refer to the default instance of a UserForm inside that form's code-behind. In other words change With AddOutgoingForm for With Me.
The Me keyword refers to the current instance - and that is what you want. Doing this will make the form work regardless of what the call site does.
Alternatively, just drop the With block altogether: With Me wouldn't be doing anything useful here.
I will assume the checkbox is indeed on your user form so just use me or you could use
AddOutgoingForm.TopBottom2.Value
But let me ask you, where is TopBottom and what is it a Boolean variable or another Checkbox? Also on the same form? The Subs are also all in the form? You have to be calling the form from somewhere so be careful between the worksheet, workbook, module variables. Is TopBottom a Global variable to the whole project (in a module called Global_Variables with Public in front of it perhaps?) You may not have access to TopBottom from inside of your form if you are not passing anything in or out.
Private Sub TopBottom2_Change()
If (Me.TopBottom2.Value) = True Then
Me.TopBottom.Value = True
Else
Me.TopBottom.Value = False
End If
End Sub
Cheers,
-WWC
In VBA, is there any known mechanism to fool the compiler into allowing the use of reserved keywords as names for class properties? For example, I would like to create a property called Select in one of my class modules. However, the compiler flags my declaration as an error. Below is the syntax I used:
Public Property Get Select() As Database.SQLStatements
End Property
Database is my VBA project name and SQLStatements is one of the class modules I created. Also, I'm running the code in MS Access 2010.
You can do that and use any keyword/reserved word in your VBA. But then it will make your code a lot messy and very hard to read/debug/maintain.
If you have a bool property named If in your class you will end up with something like this If .If Then, well, good luck reading that. Also code maintenance like Find/Replace/rename etc. will need extra caution and more work.
Anyhow, if you are willing to go through all this pain, here is how you do it.
After the keywords/reserved words add a invisible blank space using ALT+0160 and that's it. VBA will consider it a perfectly legal name. e.g. If .
Also, you will have to either use intellisense for using these propertynames or manually type the altcode everywhere. That's extra typing.
clsTest
Option Explicit
Private m_sSelect As String
Private m_bIF As Boolean
Public Property Get Select () As String '~~> Select () is actually typed as SelectALT+0160()
Select = m_sSelect
End Property
Public Property Let Select (ByVal sNewValue As String)
m_sSelect = sNewValue
End Property
Public Property Get If () As Boolean
If = m_bIF
End Property
Public Property Let If (ByVal bNewValue As Boolean)
m_bIF = bNewValue
End Property
Test Module
Option Explicit
Sub demo()
Dim objTestClass As clsTest
Set objTestClass = New clsTest
With objTestClass
.Select = "It works. But it will, for sure, create readibility/maintenance issues."
.If = False
End With
MsgBox objTestClass.Select
'/ See how hard it will to read/debug this sort of code
With objTestClass
If .If Then '~~> This line here :)
MsgBox "If prop value is TRUE"
Else
MsgBox "If prop value is FALSE"
End If
End With
End Sub
ALT+0160 <> Space
In my previous question, How do I assign a value to a property where the property name is supplied at runtime in VBA?, I learned to use CallByName to set a property in a class at run time.
This time, however, I'm trying to figure out how to get an object at run time from a string.
For example, let's say I have a string with the following data: Worksheets("RAW DATA").Range("A1").QueryTable.
Here's what I might try to do where the data above is the input for strParam below:
Function GetObject(strParam As String) As Object
GetObject = SomeFunction(strParam)
End Function
In this case, GetObject should return a QueryTable when evaluated against Worksheets("RAW DATA").Range("A1").QueryTable. Is there anything in VBA that could take the place of SomeFunction from the example above?
Active Scripting Engine can help you. Instantiate ScriptControl ActiveX, use .AddObject() method to add reference to Excel's Application object to the script control's execution environment, set the third parameter to True to make all Application's members accessible too. Then just use .Eval() method to evaluate any property or method, which is the Application's member. The example below shows evaluation of Worksheets() property:
Sub TestQueryTable()
Dim objQueryTable As QueryTable
Dim strEvalContent As String
strEvalContent = "Worksheets(""RAW DATA"").Range(""A1"").QueryTable"
Set objQueryTable = EvalObject(strEvalContent)
objQueryTable.Refresh
MsgBox objQueryTable.Connection
End Sub
Function EvalObject(strEvalContent As String) As Object
With CreateObject("ScriptControl")
.Language = "VBScript"
.AddObject "app", Application, True
Set EvalObject = .Eval(strEvalContent)
End With
End Function
If you are on 64-bit Office, this answer may help you to get ScriptControl to work.
This time you're out of luck. There is no VBA equivalent of eval (not in Excel anyway...there is in Access VBA).
(Application.Evaluate() evaluates strings as Excel expressions, not as VBA code.)
There's the "Evaluate" method (or [ ] brackets). I don't think it will do exactly what you expect - as in run VBA code found in a string. You can look it up in the VBA help menu.