A common way to get frozen columns on a Vision Power Table is to split the data across two Power Tables placed side by side. The left table holds the frozen columns and the right table holds the rest. The trouble is keeping them scrolled together. Many of these setups sync the tables from mouse-wheel events. That works until someone grabs the vertical scrollbar thumb and drags it. The dragged table moves and the other one stays put. The fix is to stop listening for the input device and listen to the viewport instead.
Skip the Usual Quick Fixes
These are the things people try first. None of them survive a scrollbar drag.
- Adding more mouse-wheel handling. A wheel handler only fires on wheel input. Dragging the thumb, clicking the scrollbar track, pressing arrow or Page keys, and selecting a row that auto-scrolls into view do not generate wheel events. Tuning the wheel handler cannot catch any of them.
- Polling the scroll position on a timer. Polling lags behind the drag, so the second table visibly trails and jitters. It also burns client CPU the whole time the window is open.
- Hunting for a scrollbar "value" binding on the component. The scroll position is Swing state inside the table's scroll pane. It is not a bindable Vision property, so no property binding reads it.
Every one of those inputs changes the same thing: the viewport's view position. The viewport fires a ChangeEvent whenever that position changes, whatever caused the change. Hook that event once and every scroll source is covered. Get it running, then fix it properly.
Check 1: Which Inputs Move the Table?
Reading: Open the window in a client or in Designer preview. Scroll the master table four ways: mouse wheel, thumb drag, scrollbar track click, and keyboard (arrow or Page Down after selecting a row). Watch whether the second table follows each time.
| Symptom | Cause | Next check |
|---|---|---|
| Follows on wheel only | Sync is driven by a mouse-wheel event, not by the viewport | Go to the procedure; replace it with a viewport ChangeListener
|
| Follows on nothing, even with a listener in place | The initialize extension function never ran |
Check 2 |
| Follows but stutters, or console prints each position two or more times | Duplicate listeners stacked on the viewport | Check 3 |
| Follows, but rows drift out of line, worst near the bottom | Tables have different geometry (row height, header, horizontal scrollbar) | Check 4 |
| Follows on every input, rows line up | Working as intended | Verification section |
Check 2: Does initialize Actually Run?
Put the listener in the Power Table's initialize extension function. This function has a quirk that fools people during testing.
It does not run in the Designer unless preview mode is already running before the window opens. If you open the window in Designer and then switch to preview, the listener was never attached. The table then behaves exactly as if the code were broken.
Reading: Add a print inside the listener's stateChanged method. Then drag the thumb and watch the output console.
- Nothing prints: Close the window. Turn preview mode on. Reopen the window with preview still active, or test in a launched client. If output now appears, the code was fine all along and only the test sequence was wrong. Go to Check 3.
- Output appears: The listener is attached. Go to Check 3.
-
Still nothing in a client: Look in the client console for a Jython traceback from
initialize. The usual causes are a typo in an import or a wrong component name. Fix it, then retest.
Check 3: Is the Listener Attached Exactly Once?
Each time initialize runs, it adds another listener instance. It does not replace the old one. During development you edit and re-run the code many times, so the viewport ends up carrying several copies. Each copy does the sync work on every scroll event, and the older copies may still run outdated logic.
Reading: Scroll one notch. Count the lines printed per position change.
- One line: Clean. Go to Check 4.
-
Two or more lines: Remove any existing listener of the same class before adding the new one. The procedure below does this. Before adding, it walks
self.viewport.changeListenersand removes anything whose class name isScrollListener.
Use that same removal loop to speed up testing. Put the code behind a test button or a propertyChange event you can toggle easily. Then you can re-attach the newest version without closing and reopening the window every time.
Check 4: Do Both Tables Share the Same Geometry?
Syncing view position copies a pixel Y offset from one table to the other. It does not copy a row index. The rows only line up if both tables lay out rows at identical pixel heights.
Reading: Scroll to the bottom and compare the last visible row in each table.
- Row heights differ. Set the same row height on both tables. Also watch for per-row height changes from configure-cell or rendering code, and make sure they apply the same way on both sides.
- Header heights differ. Give both tables the same header visibility and font, so that row 0 starts at the same Y position on screen.
- Only one table shows a horizontal scrollbar. That scrollbar takes height away from its viewport. The two tables then reach their bottom scroll limit at different offsets. Size the frozen table so it never needs a horizontal scrollbar. Alternatively, make both tables show one.
- The frozen table still shows its own vertical scrollbar. Hide it. Users should scroll through the master table only. If they can scroll both sides, you need two-way sync and the loop guard in the procedure becomes mandatory.
Once the rows line up at both the top and the bottom, move on to the procedure.
Procedure: Attach a Viewport ChangeListener and Drive the Second Table
- Pick one table as the master. This is normally the scrollable data table on the right. Note the exact component name of the frozen table. The example below uses
'Frozen Table'as a placeholder; replace it with your component's name. - Open the master Power Table's extension functions. Enable
initialize. - Paste the listener below. The base pattern reads
event.source.viewPositionfrom the viewport'sstateChangedevent. This version extends it to push the Y offset into the frozen table's viewport. - Close the window. Turn on Designer preview mode, or launch a client. Then reopen the window so that
initializeruns. - Run the four-input test from Check 1 again.
def initialize(self):
'''
Runs in a client, or in the Designer only if preview mode
is already on before the window is opened.
'''
from javax.swing.event import ChangeListener
from java.awt import Point
class ScrollListener(ChangeListener):
def __init__(self, table):
self.table = table
def stateChanged(self, event):
pos = event.source.viewPosition
# Placeholder name: use your frozen table's component name
other = self.table.parent.getComponent('Frozen Table')
if other is None:
return
otherView = other.viewport
current = otherView.viewPosition
# Guard: only move when different, prevents feedback loops
if current.y != pos.y:
otherView.viewPosition = Point(current.x, pos.y)
# Remove any previous copy before adding the new one
for listener in self.viewport.changeListeners:
if listener.__class__.__name__ == 'ScrollListener':
self.viewport.removeChangeListener(listener)
self.viewport.addChangeListener(ScrollListener(self))
Notes on the code:
- Keep the frozen table's X offset. The frozen table does not scroll horizontally. Copying the master's X offset would push its columns out of view.
- Keep the equality guard. Setting the frozen table's position fires that viewport's own change event. If you later add a mirror listener on the frozen table for two-way sync, the guard stops the two tables from endlessly re-triggering each other.
-
Keep the work inside
stateChangedlight. The listener runs on the Swing event thread and fires many times per second during a drag. Do not write to tags or run database queries from it. If other logic needs the scroll value, store it in a custom property on the window, or debounce it before sending it anywhere. -
Remove the diagnostic
printbefore commissioning. A print on every scroll event floods the client console.
Verify the Fix
- Thumb drag: Drag the master scrollbar thumb slowly from top to bottom. The frozen table must track continuously, with no lag and no catch-up jump when you release.
- Track click and keyboard: Click the scrollbar track to page. Then select a row and hold Down Arrow until the table auto-scrolls. The two tables must stay row-aligned.
- Extremes: At the very top and the very bottom, the first and last rows must match side by side. If only the bottom misaligns, go back to Check 4 and look for a horizontal scrollbar or a header height difference.
- Data refresh: Trigger a dataset update on both tables, such as a polling query or a tag change. A refresh can reset the scroll position. Confirm the tables re-align or both return to the top together.
- Reopen test: Close and reopen the window three times, then scroll one notch. The console, or a temporary counter, must show one event per change. More than one means the removal loop is not matching the class name.
FAQ
Why does my Ignition Power Table scroll sync only work with the mouse wheel?
The sync is hooked to wheel events. Dragging the thumb, clicking the track, and pressing keys never fire those events. Add a javax.swing.event.ChangeListener to self.viewport in the initialize extension function instead. It fires on every change to viewPosition, whatever caused it.
Why does the initialize extension function not run in the Ignition Designer?
initialize only runs in the Designer if preview mode is already on before the window is opened. Close the window, enable preview, then reopen it, or test in a launched client.
Why does my scroll listener fire multiple times per scroll?
Every run of initialize adds another listener instance, so they stack up during development. Before adding a new one, loop over self.viewport.changeListeners and call removeChangeListener on any listener whose __class__.__name__ is 'ScrollListener'.
When should I stop debugging the two-table sync and contact support?
Stop when the checks pass but the problem remains: the listener prints exactly once per change, row and header heights match, and rows still drift or the client console shows exceptions from the viewport. Collect the client console log, your Ignition version, and a minimal test window with two tables. Then open a case through Inductive Automation's official support channel.