I couldn't think of an appropriate title for this thread, so here's the situation:
I have a special use-case for a FlexGrid control and I currently use Krool's excellent VBFlexGrid.
I'm writing a mod-tracker like app, in which the user enters musical notes into the grid using the keyboard.
All notes,volume and instrument values are stored into an array, in a form that is understood by the sound library I'm using (FMOD). Everything works wonderfully at "design-time" but there are performance issues at "play-time"
There is a loop, where data from the array is fed to the sound library in a row-by-row fashion and an artificial delay is introduced, using the QueryPerformanceCounter. Up to this point, everything works surprisingly well, with no significant lag even at high BPMs.
It's the visual representation that I'm having issues with. I set the Row property of the control to the loop's counter and the CellEnsureVisible property to FlexVisibilityCompleteOnly. That ensures the current playing row is always visible.
My current workaround is to put those properties out of the loop and change them in a regular Timer event, with a bigger interval than the one of the "playing" interval. That works quite well but I'm sure there are better ways to do it.
I made a simple test with 2000 rows and a delay of 1ms. It takes ~2 sec without row selecting and ~6 sec with row selecting.
Any ideas?
For anyone not familiar with a tracker here's a wip shot.
@DEXWERX Although I'm only familiar with the "classic" trackers I've heard that renoise is the true evolution of trackers! I'll have to try it someday.
I'm not sure what virtual mode is. But I guess it applies when there's lots and lots of data. In my situation, playing a single 65 rows X 20 cols pattern with a couple of notes, brings the whole thing to a crawl. I don't know if I explained it well, but the grid is not databound to anything and no values are "read" from it when I hit the play button. I only need it for looks at playtime!
Everything is read from an array and played back by the FMOD api. That part works really-really fast. By commenting out a single line:
If you can post a sample project (without the fmod dependency?) that illustrates the issue, i bet Krool can figure out where the performance problem is.
Looks like the VBFlexGrid control might be a bit slow at drawing text...if you don't populate your demo with any text, I notice it runs quite a bit faster.
That said, what about a "paged" approach instead of a scroll, and only redrawing the grid intermittently? It seems to run quite a bit faster for me, but I'm not sure if it is suitable for your use case.
Code:
Option Explicit
Dim measurement As Double
Dim PlayState As Boolean
Private Declare Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As LARGE_INTEGER) As Long
Private Declare Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As LARGE_INTEGER) As Long
Private Type LARGE_INTEGER
lowpart As Long
highpart As Long
End Type
Const TWOtt = 4294967296# ' = 256# * 256# * 256# * 256#
Private Sub Form_Load()
Randomize (6)
PlayState = False
Label1.Caption = "Delay:" & scrollDelay.Value & " ms"
Dim i As Integer
For i = 1 To 500
VBFlexGrid1.RowHeight(i) = 200
VBFlexGrid1.Cell(FlexCellText, i, Int(20 * Rnd)) = Int(500 * Rnd)
Next i
End Sub
Private Sub btnStart_Click()
Dim d As Double
Dim d1 As Double
d1 = HpTimer
PlayState = True
Label2.Caption = "Started...": Label2.ForeColor = vbButtonText
Dim cntr As Integer
d = HpTimer
For cntr = 1 To 500
If chkMode Then
VBFlexGrid1.Redraw = False
VBFlexGrid1.Row = cntr
End If
Waitms scrollDelay.Value
If PlayState = False Then Exit For
If chkMode Then
If HpTimer - d > 0.012 Then
d = HpTimer
If Not VBFlexGrid1.RowIsVisible(VBFlexGrid1.Row) Then
VBFlexGrid1.TopRow = VBFlexGrid1.Row
End If
VBFlexGrid1.Redraw = True
Else
If Not VBFlexGrid1.RowIsVisible(VBFlexGrid1.Row) Then
VBFlexGrid1.TopRow = VBFlexGrid1.Row
VBFlexGrid1.Redraw = True
End If
End If
End If
Next cntr
VBFlexGrid1.Redraw = True
VBFlexGrid1.Row = 1
PlayState = False
Label2.Caption = "Stopped": Label2.ForeColor = vbRed
Debug.Print "Total time: " & HpTimer - d1
End Sub
Private Sub btnStop_Click()
PlayState = False
End Sub
Private Sub scrollDelay_Change()
Label1.Caption = "Delay:" & scrollDelay.Value & " ms"
End Sub
Private Sub Waitms(ByVal ms As Double)
Dim m_CounterStart As LARGE_INTEGER
Dim m_CounterEnd As LARGE_INTEGER
Dim m_crFrequency As Double
Dim PerfFrequency As LARGE_INTEGER
Dim crStart As Double
Dim crStop As Double
Dim TimeElapsed As Double
Dim d As Double
QueryPerformanceFrequency PerfFrequency
m_crFrequency = LI2Double(PerfFrequency)
QueryPerformanceCounter m_CounterStart
Do
QueryPerformanceCounter m_CounterEnd
crStart = LI2Double(m_CounterStart)
crStop = LI2Double(m_CounterEnd)
If 1000# * (crStop - crStart) / m_crFrequency >= ms Then Exit Do
DoEvents
Loop
End Sub
Private Function LI2Double(LI As LARGE_INTEGER) As Double
Dim Low As Double
Low = LI.lowpart
If Low < 0 Then
Low = Low + TWOtt
End If
LI2Double = LI.highpart * TWOtt + Low
End Function
Private Function HpTimer() As Double
Dim m_CounterStart As LARGE_INTEGER
Dim m_crFrequency As Double
Dim PerfFrequency As LARGE_INTEGER
QueryPerformanceFrequency PerfFrequency
m_crFrequency = LI2Double(PerfFrequency)
QueryPerformanceCounter m_CounterStart
HpTimer = LI2Double(m_CounterStart) / m_crFrequency
End Function
Might have found something else - try setting the Gridlines property FlexGridLineNone...things get much faster. So maybe some inefficiencies in the line rendering code?
Thanks for the example jpbro. I'm doing a similar thing currently but with a regular Timer control. I also expose the Timer interval to the user through a slider, so that it can be tweaked for slower machines. It's still unpredictable though and lags randomly. Disabling the grid lines though (especially if they were set to a big width) made a vast improvement.
I wish the line control could be drawn over controls, that way I could maybe mimic the gridlines of VBFlexGrid.
I will check if I can make an improvement in the gridlines.
It uses MoveToEx and LineTo API.
Currently it resets after LineTo to old position via MoveToEx. Maybe that step can only be done once after all cells are drawn. Will check..
EDIT: I could not measure any improvement by that. So..
The performance drawback comes that in your artificial waitms procedure you make a DoEvents and thus forcing the VBFlexGrid to make a full draw. And I think that is what you expect because you want to see it scrolling and selecting, right?
EDIT:
In your waitms procedure you make the following:
Code:
Do
QueryPerformanceCounter m_CounterEnd
crStart = LI2Double(m_CounterStart)
crStop = LI2Double(m_CounterEnd)
If 1000# * (crStop - crStart) / m_crFrequency >= ms Then Exit Do
DoEvents
Loop
So DoEvents is called multiple times, but it would be sufficient to make only once when the time has elapsed, like:
Code:
Do
QueryPerformanceCounter m_CounterEnd
crStart = LI2Double(m_CounterStart)
crStop = LI2Double(m_CounterEnd)
If 1000# * (crStop - crStart) / m_crFrequency >= ms Then DoEvents: Exit Do
Loop
Thanks a lot Krool! I really appreciate that you took time to look into this. It's an excellent control for the purpose it was made and in fact I compared it to MSFlexGrid in the same test and it actually performed a bit better. Not to mention the fact that the selection rectangle on the MSFlexGrid was flickering like crazy!
EDIT: Now I saw your second reply. Thank you very much, I'll try your suggestion when I get back home.
Last edited by immortalx; Jan 12th, 2018 at 06:24 AM.
Before I submitted my suggestions, I tried limiting the number of DoEvents calls as suggested but there was little to no improvement. Maybe asking the control to redraw every millisecond is just asking too much? It is somewhat surprising that the grid line thickness has such a dramatic effect over the drawing time though.
why not show a Progressbar in a cell for Playtime = 0%-100%
Code:
Option Explicit
Private Sub Form_Load()
Dim i As Long, Records As Long
Picture1.BackColor = &HC0FFFF 'for Percent Cell
Picture1.Visible = False
Records = 100
With MSFlexGrid1
.Rows = Records + 4 + 1
.Cols = 8
.FixedCols = 0
.RowHeight(0) = .RowHeight(0) * 3
.FillStyle = flexFillRepeat
For i = 4 To .Rows - 1 Step 4
.Row = i
.col = 0
.ColSel = .Cols - 1
.RowHeight(i) = 3 * Screen.TwipsPerPixelY
.CellBackColor = vbYellow
Next
.FillStyle = flexFillSingle
End With
End Sub
Private Sub Command1_Click()
With MSFlexGrid1
.Row = 15
' .TopRow = .Row
.col = 1
.LeftCol = .col
Picture1.Width = .CellWidth
Picture1.Height = .CellHeight
End With
Timer1.Interval = 100
Timer1.Enabled = True
End Sub
Public Function ShowPercent(PicBox As Control, ByVal Percent As Variant)
Dim Wert As String
Dim p As Long
p = Int(CLng(Percent))
PicBox.AutoRedraw = True
PicBox.Cls 'clear
PicBox.ScaleWidth = 100
PicBox.DrawMode = 14
Wert = Format(p, "###") & " % Played"
PicBox.CurrentX = 50 - PicBox.TextWidth(Wert) / 2
PicBox.CurrentY = (PicBox.ScaleHeight - PicBox.TextHeight(Wert)) / 2
PicBox.Print Wert
PicBox.Line (-2, -2)-(Int(p), PicBox.ScaleHeight), vbBlue, BF
PicBox.Refresh
End Function
Private Sub Timer1_Timer()
Static z As Long
z = z + 1
If z > 100 Then
Timer1.Enabled = False
Set MSFlexGrid1.CellPicture = Nothing
z = 0
Exit Sub
End If
ShowPercent Picture1, z
With MSFlexGrid1
Set .CellPicture = Picture1.Image
End With
End Sub
regards
Chris
to hunt a species to extinction is not logical !
since 2010 the number of Tigers are rising again in 2016 - 3900 were counted. with Baby Callas it's 3901, my wife and I had 2-3 months the privilege of raising a Baby Tiger.
As others have suggested already, the Refresh-Frequency of the Grid has to be reduced.
Drastically.
Why should one force some complex Control to refresh itself with "1000 FPS" (like in a "Game-Engine on steroids"),
when the Screen-Refresh-Rate of a typical LCD-Screen is only 50-70Hz?
Let's say the LCD refreshes with 50Hz (for easier calculation-ratios).
In that case the Graphics-Driver will have refreshed the Display only 100 times in the desired 2-seconds timespan.
So from those 2000-Repainting-enforcements into Krools OffScreen-Bitmap - only 5% were really necessary (were producing a "visual effect" on the screen).
For a "fluent viewing" from the users end, even 25Hz are entirely sufficient (your typical movie runs with 24hz on the "silverscreen").
That said, let's adapt "the loop" approriately (with only a few small changes from the posted original):
Code:
Private Sub btnStart_Click()
PlayState = True
Label2.Caption = "Started...": Label2.ForeColor = vbButtonText
Dim cntr As Integer, RefrCount As Long
For cntr = 1 To 2000
If chkMode.Value = 1 And cntr Mod 40 = 0 Then
VBFlexGrid1.Row = cntr
VBFlexGrid1.CellEnsureVisible FlexVisibilityCompleteOnly
RefrCount = RefrCount + 1 'just a RefreshCounter
End If
Waitms scrollDelay.Value
If PlayState = False Then Exit For
Next cntr
VBFlexGrid1.Row = 1
VBFlexGrid1.CellEnsureVisible FlexVisibilityCompleteOnly 'not sure if that's wanted - but let's reflect the new RowCursor-Position also on loop-exit
Caption = RefrCount 'reflect, how often we've updated the Grid within the two seconds
PlayState = False
Label2.Caption = "Stopped": Label2.ForeColor = vbRed
End Sub
With that, the code runs only marginally slower on my machine, compared to "unchecked" mode.
@ChrisE thank you for the example. Although it doesn't scroll the grid (which is a must unfortunately) it'll be needed elsewhere.
@Schmidt Thank you, that seems like a very elegant solution. I was currently using a Timer control (with a greater interval) to refresh the grid separately from the main loop, but your solution kills 2 birds with one stone and it seems to produce more stable results.
The other nice thing is i can set the mod divisor to a variable depending on the BPM.
As others have suggested already, the Refresh-Frequency of the Grid has to be reduced.
Drastically.
Why should one force some complex Control to refresh itself with "1000 FPS" (like in a "Game-Engine on steroids"),
when the Screen-Refresh-Rate of a typical LCD-Screen is only 50-70Hz?
Let's say the LCD refreshes with 50Hz (for easier calculation-ratios).
In that case the Graphics-Driver will have refreshed the Display only 100 times in the desired 2-seconds timespan.
So from those 2000-Repainting-enforcements into Krools OffScreen-Bitmap - only 5% were really necessary (were producing a "visual effect" on the screen).
For a "fluent viewing" from the users end, even 25Hz are entirely sufficient (your typical movie runs with 24hz on the "silverscreen").
That said, let's adapt "the loop" approriately (with only a few small changes from the posted original):
Code:
Private Sub btnStart_Click()
PlayState = True
Label2.Caption = "Started...": Label2.ForeColor = vbButtonText
Dim cntr As Integer, RefrCount As Long
For cntr = 1 To 2000
If chkMode.Value = 1 And cntr Mod 40 = 0 Then
VBFlexGrid1.Row = cntr
VBFlexGrid1.CellEnsureVisible FlexVisibilityCompleteOnly
RefrCount = RefrCount + 1 'just a RefreshCounter
End If
Waitms scrollDelay.Value
If PlayState = False Then Exit For
Next cntr
VBFlexGrid1.Row = 1
VBFlexGrid1.CellEnsureVisible FlexVisibilityCompleteOnly 'not sure if that's wanted - but let's reflect the new RowCursor-Position also on loop-exit
Caption = RefrCount 'reflect, how often we've updated the Grid within the two seconds
PlayState = False
Label2.Caption = "Stopped": Label2.ForeColor = vbRed
End Sub
With that, the code runs only marginally slower on my machine, compared to "unchecked" mode.
HTH
Olaf
You are forgetting that each of those 24 frames per second is shown twice (Film) or three times (Digital). So the minimum Flicker rate has always been 48Hz.
You are forgetting that each of those 24 frames per second is shown twice (Film) or three times (Digital). So the minimum Flicker rate has always been 48Hz.
If your Grid-Update-frequency is 25 Hz - and your TFT-Screen refreshes itself with 75Hz, then each "Grid-Frame is shown 3 times as well.