2

I'd like to be able to copy a cell and paste ONLY number formats. Unfortunately, there is no built-in option in the PasteSpecial command.

Is there a way to press the copy button, select some destination cells, run a macro, and be able to retrieve the copied cells in a way analogous to the Selection object in VBA so that I can use its properties?

The only alternative I can think of is pasting to a known empty range (very far away) and then using that intermediate range, as below:

Dim A As Range
Set A = Range("ZZ99999")
A.PasteSpecial Paste:=xlPasteAll
Selection.NumberFormat = A.NumberFormat

Thanks!

ecksc
  • 148
  • 2
  • 9
  • Unless I've misread your question, could you not use format painter? `Selection.PasteSpecial Paste:=xlPasteFormats, Operation:=xlNone, SkipBlanks:=False, Transpose:=False` – Gareth Apr 16 '14 at 14:32
  • @Gareth That would copy e.g. colors too. – GSerg Apr 16 '14 at 14:34
  • Doesn't that change other formatting like background, borders, etc. (not just number formats)? – ecksc Apr 16 '14 at 14:34
  • First, paste the values or simply transfer them like `Range("B10:B20").Value = Range("A10:A20").Value`, then assign the number format the same way. `Range("A10:A20").NumberFormat = Range("B10").NumberFormat`. – David Zemens Apr 16 '14 at 14:42
  • I guess I should mention that the actual macro I end up writing would be more complicated than above code (e.g. with logic to replicate the same behavior as the "paste" command). My concern is getting the copied range, and then I can incorporate that logic. – ecksc Apr 16 '14 at 15:20
  • Then code you want is [here](http://www.codeproject.com/Articles/149009/Getting-the-Excel-Range-object-from-the-Clipboard), but for .NET. I'm looking into porting it. – GSerg Apr 16 '14 at 16:06

2 Answers2

3

Find olelib.tlb on the Internet (Edanmo's OLE interfaces & functions). There should be plenty of download links. Download and reference from your VBA project (Tools - References).

Note that it does not contain any executable code, only declarations of OLE functions and interfaces.

Also you might notice it's quite big, about 550kb. You can extract only the needed interfaces from it and recompile to get a much lighter TLB file, but that is up to you.
(If you are really unhappy with a TLB, there is also the dark magic route where you don't need any TLBs at all because you create assembly stubs on the fly to call vTable methods directly, but I won't be feeling like porting the below code this way.)

Then create a helper module and put this code into it:

Option Explicit

' No point in #If VBA7 and PtrSafe, as the Edanmo's olelib is 32-bit
Private Declare Function OpenClipboard Lib "user32.dll" (ByVal hwnd As Long) As Long
Private Declare Function GetClipboardData Lib "user32.dll" (ByVal wFormat As Long) As Long
Private Declare Function CloseClipboard Lib "user32.dll" () As Long


Public Function GetCopiedRange() As Excel.Range

  Dim CF_LINKSOURCE As Long
  CF_LINKSOURCE = olelib.RegisterClipboardFormat("Link Source")
  If CF_LINKSOURCE = 0 Then Err.Raise 5, , "Failed to obtain clipboard format CF_LINKSOURCE"

  If OpenClipboard(0) = 0 Then Err.Raise 5, , "Failed to open clipboard."


  On Error GoTo cleanup

  Dim hGlobal As Long
  hGlobal = GetClipboardData(CF_LINKSOURCE)

  If hGlobal = 0 Then Err.Raise 5, , "Failed to get data from clipboard."

  Dim pStream As olelib.IStream
  Set pStream = olelib.CreateStreamOnHGlobal(hGlobal, 0)

  Dim IID_Moniker As olelib.UUID
  olelib.CLSIDFromString "{0000000f-0000-0000-C000-000000000046}", IID_Moniker

  Dim pMoniker As olelib.IMoniker
  olelib.OleLoadFromStream pStream, IID_Moniker, pMoniker


  Set GetCopiedRange = RangeFromCompositeMoniker(pMoniker)

cleanup:
  Set pMoniker = Nothing 'To make sure moniker releases before the stream

  CloseClipboard
  If Err.Number > 0 Then Err.Raise Err.Number, Err.Source, Err.Description, Err.HelpFile, Err.HelpContext

End Function


Private Function RangeFromCompositeMoniker(ByVal pCompositeMoniker As olelib.IMoniker) As Excel.Range
  Dim monikers() As olelib.IMoniker
  monikers = SplitCompositeMoniker(pCompositeMoniker)

  If UBound(monikers) - LBound(monikers) + 1 <> 2 Then Err.Raise 5, , "Invalid composite moniker."

  Dim binding_context As olelib.IBindCtx
  Set binding_context = olelib.CreateBindCtx(0)

  Dim WorkbookUUID As olelib.UUID
  olelib.CLSIDFromString "{000208DA-0000-0000-C000-000000000046}", WorkbookUUID

  Dim wb As Excel.Workbook
  monikers(LBound(monikers)).BindToObject binding_context, Nothing, WorkbookUUID, wb

  Dim pDisplayName As Long
  pDisplayName = monikers(LBound(monikers) + 1).GetDisplayName(binding_context, Nothing)

  Dim raw_range_name As String ' Contains address in the form of "!SheetName!R1C1Local", need to convert to non-local
  raw_range_name = olelib.SysAllocString(pDisplayName)
  olelib.CoGetMalloc(1).Free pDisplayName

  Dim split_range_name() As String
  split_range_name = Split(raw_range_name, "!")

  Dim worksheet_name As String, range_address As String
  worksheet_name = split_range_name(LBound(split_range_name) + 1)
  range_address = Application.ConvertFormula(ConvertR1C1LocalAddressToR1C1(split_range_name(LBound(split_range_name) + 2)), xlR1C1, xlA1)

  Set RangeFromCompositeMoniker = wb.Worksheets(worksheet_name).Range(range_address)

End Function

Private Function SplitCompositeMoniker(ByVal pCompositeMoniker As olelib.IMoniker) As olelib.IMoniker()

  Dim MonikerList As New Collection
  Dim enumMoniker As olelib.IEnumMoniker

  Set enumMoniker = pCompositeMoniker.Enum(True)

  If enumMoniker Is Nothing Then Err.Raise 5, , "IMoniker is not composite"

  Dim currentMoniker As olelib.IMoniker
  Do While enumMoniker.Next(1, currentMoniker) = olelib.S_OK
    MonikerList.Add currentMoniker
  Loop

  If MonikerList.Count > 0 Then
    Dim res() As olelib.IMoniker
    ReDim res(1 To MonikerList.Count)

    Dim i As Long
    For i = 1 To MonikerList.Count
      Set res(i) = MonikerList(i)
    Next

    SplitCompositeMoniker = res
  Else
    Err.Raise 5, , "No monikers found in the composite moniker."
  End If

End Function

Private Function ConvertR1C1LocalAddressToR1C1(ByVal R1C1LocalAddress As String) As String
  ' Being extra careful here and not doing simple Replace(Replace()),
  ' because e.g. non-localized row letter may be equal to localized column letter which will lead to double replace.
  Dim row_letter_local As String, column_letter_local As String
  row_letter_local = Application.International(xlUpperCaseRowLetter)
  column_letter_local = Application.International(xlUpperCaseColumnLetter)

  Dim row_letter_pos As Long, column_letter_pos As Long
  row_letter_pos = InStr(1, R1C1LocalAddress, row_letter_local, vbTextCompare)
  column_letter_pos = InStr(1, R1C1LocalAddress, column_letter_local, vbTextCompare)

  If row_letter_pos = 0 Or column_letter_pos = 0 Or row_letter_pos >= column_letter_pos Then Err.Raise 5, , "Invalid R1C1Local address"

  If Len(row_letter_local) = 1 And Len(column_letter_local) = 1 Then
    Mid$(R1C1LocalAddress, row_letter_pos, 1) = "R"
    Mid$(R1C1LocalAddress, column_letter_pos, 1) = "C"
    ConvertR1C1LocalAddressToR1C1 = R1C1LocalAddress
  Else
    ConvertR1C1LocalAddressToR1C1 = "R" & Mid$(R1C1LocalAddress, row_letter_pos + Len(row_letter_local), column_letter_pos - (row_letter_pos + Len(row_letter_local))) & "C" & Mid$(R1C1LocalAddress, column_letter_pos + Len(column_letter_local))
  End If
End Function

Credits go to Alexey Merson.

GSerg
  • 76,472
  • 17
  • 159
  • 346
  • Wow, that's awesome! Great job. – ecksc Apr 16 '14 at 20:35
  • 1
    WOW, The Saint Graal of any Excel VBA programmer! I just had to add a slight modification since the raw_range_name is like "!Sheet1!L1C1" in my case while the correct syntax with xlR1C1 would be "!Sheet1!R1C1", so the call to Application.ConvertFormula always failed with "Error 1004: Application-defined error". I still can't believe I missed this post for 4 years... – hymced Dec 11 '18 at 16:45
  • @GSerg I have been using your code in a simple procedure that pastes a SUM() formula referencing the copied range as argument. But using a copied range containing multiple `Areas`, Excel seems to "remember" the cells that have actually been copied, while using the CompositeMoniker method from Alexey Merson, the returned range is delimited by the upper left cell and the lower right cell of the copied range. Which results in having more cells than initially copied... Do you know if this can be tackled ? I cannot workaround this with IMoniker (maybe its not Excel native way of doinf it...) – hymced Feb 21 '19 at 11:05
  • @hymced I have no idea. I see you have already asked that at codeproject, that is probably your best chance. – GSerg Feb 21 '19 at 11:28
  • 1
    Another method here: https://stackoverflow.com/a/60033558/2981328, still based on the "Link" format, produces the same result: cells between the copied areas are returned in the copied range even if not really copied... – hymced Nov 28 '20 at 23:45
  • this code is not working in **64bit excel** eventhough u have mentioned no need to convert the code into 32bit, pls help? Also how can i use this code, basically i just need copied range(marching ants border) in **msgBox** or **Debug.print** @GSerg – Zohir Emon Oct 27 '22 at 09:24
  • @ZohirEmon I didn't mention no need to convert, I said that the *Edanmo's olelib* used here is 32-bit only, so there's no *point* to convert. If you can make/find a 64-bit equivalent of the interface declarations, you should reference that instead. – GSerg Oct 27 '22 at 12:19
  • @GSerg thanks for your comments. If i use the code in 32bit then how can i get the copied range in **msgBox**? – Zohir Emon Oct 27 '22 at 13:54
0

Here's one way. Obviously you'll have to change the range to suit your situation, but it should get you the general idea:

Dim foo As Variant

foo = Sheet1.Range("A1:A10").NumberFormat

Sheet1.Range("D1:D10").NumberFormat = foo

Which really can be simplified to:

Sheet1.Range("D1:D10").NumberFormat = Sheet1.Range("A1:A10").NumberFormat

and if all of your formats in the range are the same, you can just do:

Sheet1.Range("D1:D10").NumberFormat = Sheet1.Range("A1").NumberFormat

Enough rambling...you get the idea.

sous2817
  • 3,915
  • 2
  • 33
  • 34
  • 1
    `NumberFormat` returns `Null` if not all cells have same number format. Also, it is not a problem to copy the format, the problem is to figure what range has been copied before pressing the macro button. – GSerg Apr 16 '14 at 14:43
  • 1
    it works only if _all_ cells in range `A1:A10` has _same_ number format – Dmitry Pavliv Apr 16 '14 at 14:43
  • @GSerg *the problem is to figure what range has been copied before pressing the macro button.* well the solution to that is to include the "copy" as part of the procedure, and to **not** rely on user's `Selection`... – David Zemens Apr 16 '14 at 14:44
  • I'm trying to get it so that the user can use the Excel copy function(Ctrl+C), select another range, and then have the macro grab that copied range somehow and store it in a variable so that it can be manipulated. As far as I can tell, the above hardcodes the range, which isn't what I'm looking for. – ecksc Apr 16 '14 at 14:46
  • @DavidZemens You need two ranges, source and target. You first select target, then click Copy (standard Excel button), the range will get a border around it. Then you select target range and click the macro button. – GSerg Apr 16 '14 at 14:47
  • @ecksc The hardcoded range is there as an example, as it's often times hard to give a complete solution given the limited context. Your question was how to copy just the number format, which is the answer that I provided. I even called out the fact that it will have to be amended to suit your situation. If your requirements have changed from your original ask, you should probably edit your question to better fit what you're after. – sous2817 Apr 16 '14 at 14:49
  • @GSerg I know exactly how to do these things... I'm saying the root of the problem you're describing (and why you're doing this, since you're not the OP, I don't really understand) is in the expected use, i.e., "select and manually copy" and then select another range and invoke the macro. Better to write a macro that prompts user for input ranges (target, source, etc.). – David Zemens Apr 16 '14 at 14:50
  • @sous2817 The original question is good enough. Please read the second paragraph. – GSerg Apr 16 '14 at 14:51
  • @David-Zemens I realize that I could create a program that prompts for the ranges. Sorry if my purpose isn't more clear. The reason I want to grab the copied selection is to make the operation as fast and convenient as possible, and to avoid needing more than one key shortcut for the entire operation. – ecksc Apr 16 '14 at 15:15
  • Well that is one way. Another way might be, if you know based on the selection where the data *should* go, you could use the `Offset` method, etc. There is usually some structure to a worksheet and there are usually ways to determine the destination range, if we know enough about the workbook and what you're doing with it, etc.. Unless you're really after a solution that allows the users to paste the data *anywhere* (this is rarely the case, but it might be for you)... – David Zemens Apr 16 '14 at 15:25
  • @DavidZemens I'm looking for the user being able to paste the data anywhere. My only other thought is that if I could store a "default" number format in a separate workbook that is usually hidden, I could at least get close to what I want (but that only works if you want the same number format in all of your locations which you change manually as needed). – ecksc Apr 16 '14 at 15:38
  • Is there need to "store" a default format? You could just *apply* whatever default you desire. If you need to store the state of the copied range's number format (from the first cell in that range), you could do so in a `Named Range` which can be used like a "variable" within the worksheet. Since `NumberFormat` property is a string, you could easily store it in a named range and access it that way. – David Zemens Apr 16 '14 at 16:23
  • @DavidZemens Thanks, that way works as well. I thought it might be more user friendly to use the custom formatting tool to set a format in the default box (or just paste to that cell), rather than a string that you could easily typo or copy incorrectly. – ecksc Apr 16 '14 at 16:34
  • I'm not sure you understand what I'm suggesting. The idea is to store whatever `NumberFormat` property is assigned (how it is initially assigned to the source range is inconsequential, that is outside the scope of the macro). Then, the macro will "read" the property from the first cell, and save it in a named range, which you could use in any other macros, later, as needed. – David Zemens Apr 16 '14 at 16:55