1

I am now trying to achieve something like the query function in Google Sheets. Obviously in this GIF, someone has already done that. I wonder how they could do that in Excel / VBA.

My specific question is: in VBA, how to fill other cells' formulas by entering a formula in a specific cell? (replicate the function used in this GIF and not using VBA + advanced filter)

enter image description here

  1. Enter a formula in cell A3
  2. Press CTRL + SHIFT + ENTER
  3. Receive results

This is what I got so far:

Học Excel Online mi_sql

The code in a standard module:

Sub run_sql_sub(sql)
On Error Resume Next
Set cn = CreateObject("ADODB.Connection")
Set rs = CreateObject("ADODB.Recordset")

With cn
    .ConnectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data     Source=" & _
    This Workbook.FullName _
& ";Extended Properties=""Excel 12.0 Xml;HDR=YES;IMEX=1"";"
    .Open
End With
rs.Open sql, cn

Application.ScreenUpdating = False
ActiveSheet.Range("A1:XFD1048576").ClearContents

For intColIndex = 0 To rs.Fields.Count - 1
    Range("A1").Offset(0, intColIndex).Value = rs.Fields(intColIndex).Name
Next

Range("A2").CopyFromRecordset rs
Application.ScreenUpdating = True
rs.Close: cn.Close: Set rs = Nothing: Set cn = Nothing
End Sub

And this code is in activesheet's module:

Private Sub Worksheet_Change(ByVal Target As Range)
    Dim KeyCells As Range

    Set KeyCells = ActiveSheet.Range("A1")

    If Not Application.Intersect(KeyCells, Range(Target.Address)) _
       Is Nothing Then

        If InStr(KeyCells.Value2, "mi_sql") > 0 Then
            sql = Right(KeyCells.Value2, Len(KeyCells.Value2) - Len("mi_sql "))
            run_sql_sub sql
        End If
    End If
End Sub

Update 08.04.2019: found a solution

' Code in standard Module
Public collectCal As Collection
Private ccal As CallerCal

Sub subResizeKQ(caller As CallerInfo)
    On Error Resume Next
    Application.EnableEvents = False
    If caller.Id <> "" Then
        Application.Range(caller.Id).ClearContents
        Application.Range(caller.Id).Resize(caller.rows, caller.cols).FormulaArray = caller.FomulaText
    End If
    Application.EnableEvents = True
End Sub


Function ResizeKQ(value As Variant) As Variant
    If ccal Is Nothing Then Set ccal = New CallerCal
    If collectCal Is Nothing Then Set collectCal = New Collection

    Dim caller As New CallerInfo
    Dim rows As Long, cols As Long
    Dim arr As Variant
    arr = value
    rows = UBound(arr, 1) - LBound(arr, 1) + 1
    cols = UBound(arr, 2) - LBound(arr, 2) + 1

    Dim rgcaller As Range
    Set rgcaller = Application.caller
    caller.Id = rgcaller.Address(True, True, xlA1, True, True)
    caller.rows = rgcaller.rows.Count
    caller.cols = rgcaller.Columns.Count
    caller.FomulaText = rgcaller.Resize(1, 1).Formula

    If caller.rows <> rows Or caller.cols <> cols Then
        caller.rows = rows
        caller.cols = cols
        collectCal.Add caller, caller.Id
    End If
    ResizeKQ = arr
End Function

Function fRandArray(numRow As Long, numCol As Long) As Variant
    Application.Volatile True
    ReDim arr(1 To numRow, 1 To numCol)
    For i = 1 To numRow
        For j = 1 To numCol
            arr(i, j) = Rnd
        Next
    Next
    fRandArray = ResizeKQ(arr)
End Function

'--------------------------------------------------------------------------
' code in Class Module name CallerCal

Private WithEvents AppEx As Application

Private Sub AppEx_SheetCalculate(ByVal Sh As Object)
    Dim caller As CallerInfo
    If collectCal Is Nothing Then Exit Sub
    For Each caller In collectCal
        subResizeKQ caller
        collectCal.Remove caller.Id
        Set caller = Nothing
    Next
    Set collectCal = Nothing
End Sub

Private Sub Class_Initialize()
    Set AppEx = Application
End Sub

Private Sub Class_Terminate()
     Set AppEx = Nothing
End Sub

'--------------------------------------------------------------------------
' code in Class Module name CallerInfo

Public rows As Long

Public cols As Long

Public Id As String

Public FomulaText As String

To test it, go to Excel Sheet, enter the following test formula in A1:

=fRandArray(10,10)

P.S: If anyone is using Excel 365 Insider Program, Microsoft has published this kind of formula called Dynamic Array Function: https://support.office.com/en-ie/article/dynamic-arrays-and-spilled-array-behavior-205c6b06-03ba-4151-89a1-87a7eb36e531

Đức Thanh Nguyễn
  • 9,127
  • 3
  • 21
  • 27
  • Use advance filter. There are many tutorials on how to make this happen with vba. – Scott Craner Jan 19 '17 at 17:52
  • Thanks for your reply, I asked: in VBA, and by entering a formula. not by using advanced filter + VBA – Đức Thanh Nguyễn Jan 19 '17 at 17:53
  • I know what you asked, the closest answer is to use Advanced Filter, that is how Excel does it naturally. What you are seeing in the gif is an add-on and not something that can be done without a lot of programming. I would suggest if you want what you see, then pay for the add-on. – Scott Craner Jan 19 '17 at 17:57
  • I'm fine with a lot of programming and some years ago I wrote something similar to this but I need to put "the triggering formula" to a different cell. And yes, it is from an excel add-in. – Đức Thanh Nguyễn Jan 19 '17 at 18:00
  • Then please post your attempt and tell use specifically where it is failing. As you know SO is not a code for me site. – Scott Craner Jan 19 '17 at 18:03
  • Put some data and expected output. We have to use multiple formulas as array formula to achieve that. – Harun24hr Jan 19 '17 at 18:08
  • @ScottCraner It did not fail, I'm just curious how they can change other cells' properties in a UDF Function. And as you wish, this is the code for what I've achieved so far and it is not a lot of programming. – Đức Thanh Nguyễn Jan 19 '17 at 18:11
  • Not sure I understand your problem. As it seems to be doing what you want. Just an FYI a UDF cannot change the cell properties of another cell. Could you explain more what it is that your code does not do that you want. – Scott Craner Jan 19 '17 at 18:20
  • Hi @ScottCraner, actually I do know about the catch with UDF cannot change the cell properties of another cell. But there are ways that you can change the value of other cells: http://stackoverflow.com/questions/23433096/using-a-udf-in-excel-to-update-the-worksheet – Đức Thanh Nguyễn Jan 19 '17 at 18:30
  • Yes, that loop hole has been closed with 2013 and later. so what would you expected output be, what do you want to change? Your code is not a UDF as you have it anyways, but a SUB based on an Event, which is different. – Scott Craner Jan 19 '17 at 18:33
  • @ĐứcThanhNguyễn Actually, I'm pretty sure I know how the original gif was done: http://www.cpearson.com/excel/returningarraysfromvba.aspx. Notice that the cell was run with CTRL+SHIFT+ENTER to make it an array formula (you can tell by the braces around the whole formula "{ ... }"). Notice also that when they move from cell to cell, each cell has that same formula. This means that they must have to drag the formula across the whole range they want before-hand. Then they just redefine the upper-left cell formula to regenerate all the results..... – Blackhawk Jan 23 '17 at 22:27
  • @ĐứcThanhNguyễn ....If you're interested, I could write code to duplicate that behavior, but because it requires that the whole range be defined before-hand, it's much less useful IMO than the solution I already posted. – Blackhawk Jan 23 '17 at 22:28
  • @ĐứcThanhNguyễn incidentally, the premise in the title of your question is wrong, I believe. I don't think that the original gif only placed the formula in one cell. I believe that his `bs_sql()` function just displays a blank if no argument is provided. This is why the rest of the cells appear blank. However, I believe that all the cells in the range were predefined with `=bs_sql()` before he started recording the gif and changed the parameter of the upper-left cell (which has the effect of updating the rest as well). – Blackhawk Jan 24 '17 at 15:35
  • @Blackhawk, I found something, this could be interesting for you https://www.codeproject.com/Articles/11358/Excel-user-defined-functions-unlimited – Đức Thanh Nguyễn Aug 29 '17 at 17:27
  • @ĐứcThanhNguyễn Thanks! I unfortunately have to avoid compiled code solutions because of my work environment, but it's always cool to see what is possible (and I miss C++ :'( ) – Blackhawk Aug 31 '17 at 15:10
  • @ScottCraner I have found my resolution. It is pure VBA and it is not a lot of code – Đức Thanh Nguyễn Apr 08 '19 at 19:43

1 Answers1

0

I agree with the other comments - MS doesn't seem to provide a way to do this natively, and any way of doing it directly would probably involve some Excel-breaking memory manipulation.

However...

I suggest taking your method one step further and generalizing it

Copy and paste this class into a text file, then import it into VBA (which allows Attribute VB_PreDeclaredID = True and Attribute Item.VB_UserMemId = 0):

RangeEdit

VERSION 1.0 CLASS
BEGIN
  MultiUse = -1  'True
END
Attribute VB_Name = "RangeEdit"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
Private colRanges As Collection
Private colValues As Collection

Private Sub Class_Initialize()
    Set colRanges = New Collection
    Set colValues = New Collection
End Sub

Public Property Let Item(rng_or_address As Variant, value As Variant)
Attribute Item.VB_UserMemId = 0
    colRanges.Add rng_or_address
    colValues.Add value
End Property

Public Sub flush(sh As Worksheet)
    Application.EnableEvents = False
    While colRanges.Count > 0

        If TypeName(colRanges(1)) = "Range" Then
            colRanges(1).value = colValues(1)
        ElseIf TypeName(colRanges(1)) = "String" Then
            sh.Range(colRanges(1)).value = colValues(1)
        End If
        colRanges.Remove 1
        colValues.Remove 1

    Wend
    Application.EnableEvents = True
End Sub

Make your Workbook_SheetChange method the following:

Private Sub Workbook_SheetChange(ByVal sh As Object, ByVal Target As Range)
    RangeEdit.flush sh
End Sub

Now you can create a UDF that modifies other cells. The way it works is it queues up all the modifications you make and only runs them after the cell loses focus. The syntax allows you to treat it almost like the regular Range function. You can run it either with an address string or with an actual range (though you might want to add an error if it's not either one of those).

Here is a quick example UDF that can be run from an Excel cell formula:

Public Function MyUDF()
    RangeEdit("A1") = 4
    RangeEdit("B1") = 6
    RangeEdit("C4") = "Hello everyone!"

    Dim r As Range
    Set r = Range("B12")

    RangeEdit(r) = "This is a test of using a range variable"

End Function

For your specific case, I would replace

For intColIndex = 0 To rs.Fields.Count - 1
    Range("A1").Offset(0, intColIndex).Value = rs.Fields(intColIndex).Name
Next

with

For intColIndex = 0 To rs.Fields.Count - 1
    RangeEdit(Range("A1").Offset(0, intColIndex)) = rs.Fields(intColIndex).Name
Next

And to copy the recordset I would define the following function (it assumes that the recordset cursor is set to the first record. if you Move it previously you might want to have rs.MoveFirst in there):

Public Sub FormulaSafeRecordsetCopy(rs As ADODB.Recordset, rng As Range)
    Dim intColIndex As Long
    Dim intRowIndex As Long
    While Not rs.EOF
        For intColIndex = 0 To rs.Fields.Count - 1
            RangeEdit(rng.Offset(intRowIndex, intColIndex)) = rs.Fields(intColIndex).value
        Next
        rs.MoveNext
        intRowIndex = intRowIndex + 1
    Wend
End Sub
Blackhawk
  • 5,984
  • 4
  • 27
  • 56