【问题标题】:NSTableView reloadData method causes NSProgressIndicator in all rows to update and flickerNSTableView reloadData 方法导致所有行中的 NSProgressIndicator 更新并闪烁
【发布时间】:2017-10-19 00:16:58
【问题描述】:

注意:我已经在 Stack Overflow 上搜索过类似的问题,但我发现的问题似乎都没有解决这个特定问题。

我编写了一个小示例应用程序(完整的 Xcode 项目,源代码可在此处获得:http://jollyroger.kicks-ass.org/stackoverflow/FlickeringTableView.zip),它顺序播放 /System/Library/Sounds/ 中的所有声音,并在播放声音时在窗口中显示这些声音以显示我看到的问题。 MainMenu.xib 中的窗口有一个单列NSTableView,其中一行定义为单元格模板,其中包含三个项目:

  • NSTextField 保存声音名称
  • 另一个NSTextField 保存声音细节
  • NSProgressIndicator 在声音播放时显示播放进度

我已将NSTableCellView (SoundsTableCellView.h) 子类化以定义单元格视图中的每个项目,以便我可以在时间到时访问和设置它们。

我定义了一个MySound 类,它封装了通过AVAudioPlayer API 处理声音文件播放所需的属性和方法。此类定义了一个MySoundDelegate 协议,以允许应用代理在声音开始或结束播放时接收事件。

应用程序委托遵守NSTableViewDelegateNSTableViewDataSource 协议,允许它以MySound 对象数组的形式存储表数据,并在需要时使用相关信息更新表。它还遵守MySoundDelegate 协议,以在声音开始或结束播放时接收事件。该委托还有一个NSTimer 任务,它定期调用refreshWindow 方法来更新当前播放声音的进度指示器。

应用程序代理的refreshWindow 方法根据列表中的声音数量显示和调整窗口大小,并更新存储的对正在播放的声音的关联NSProgressIndicator 的引用。

调用应用委托的tableView: viewForTableColumnNSTableViewDelegate 协议)方法来填充表格单元格。在其中,我使用 Apple 的标准“以编程方式填充表视图”建议:

  1. 检查表列标识符以确保它与 标识符 (sound column) 我在 Interface Builder (Xcode) 中为表格列设置,
  2. 通过调用thisTableView makeViewWithIdentifier获取标识符为(sound cell)的对应表格单元格,
  3. 使用传入的row参数定位匹配的数组元素 数据源(应用委托sounds数组),然后
  4. 设置NSTextFields的字符串值,并将单元格中NSProgressIndicatormaxValuedoubleValue设置为关联声音对象的对应细节,
  5. 在关联的声音对象中存储对关联NSProgressIndicator 控件的引用以供以后更新

这是viewForTableColumn 方法:

- (NSView *)tableView:(NSTableView *)thisTableView viewForTableColumn:(NSTableColumn *)thisTableColumn row:(NSInteger)thisRow
{
    SoundsTableCellView *cellView = nil;

    // get the table column identifier

    NSString *columnID = [thisTableColumn identifier];
    if ([columnID isEqualToString:@"sound column"])
    {
        // get the sound corresponding to the specified row (sounds array index)

        MySound *sound = [sounds objectAtIndex:thisRow];

        // get an existing cell from IB with our hard-coded identifier

        cellView = [thisTableView makeViewWithIdentifier:@"sound cell" owner:self];

        // display sound name

        [cellView.soundName setStringValue:[sound name]];
        [cellView.soundName setLineBreakMode:NSLineBreakByTruncatingMiddle];

        // display sound details (source URL)

        NSString *details = [NSString stringWithFormat:@"%@", [sound sourceURL]];
        [cellView.soundDetails setStringValue:details];
        [cellView.soundDetails setLineBreakMode:NSLineBreakByTruncatingMiddle];

        // update progress indicators

        switch ([sound state])
        {
            case kMySoundStateQueued:
                break;
            case kMySoundStateReadyToPlay:
                break;
            case kMySoundStatePlaying:
                if (sound.playProgress == nil)
                {
                    sound.playProgress = cellView.playProgress;
                }

                NSTimeInterval duration = [sound duration];
                NSTimeInterval position = [sound position];

                NSLog(@"row %ld: %@ (%f / %f)", (long)thisRow, [sound name], position, duration);
                NSLog(@"         %@: %@", [sound name], sound.playProgress);

                [cellView.playProgress setMaxValue:duration];
                [cellView.playProgress setDoubleValue:position];

                break;
            case kMySoundStatePaused:
                break;
            case kMySoundStateFinishedPlaying:
                break;
            default:
                break;
        }
    }

    return cellView;
}

这是refreshWindow 方法:

- (void) refreshWindow
{
    if ([sounds count] > 0)
    {
        // show window if needed

        if ([window isVisible] == false)
        {
            [window makeKeyAndOrderFront:self];
        }

        // resize window to fit all sounds in the list if needed

        NSRect frame = [self.window frame];

        int screenHeight = self.window.screen.frame.size.height;

        long maxRows = ((screenHeight - 22) / 82) - 1;
        long displayedRows = ([sounds count] > maxRows ? maxRows : [sounds count]);

        long actualHeight = frame.size.height;
        long desiredHeight = 22 + (82 * displayedRows);
        long delta = desiredHeight - actualHeight;

        if (delta != 0)
        {
            frame.size.height += delta;
            frame.origin.y -= delta;

            [self.window setFrame:frame display:YES];
        }

        // update play position of progress indicator for all sounds in the list

        for (MySound *nextSound in sounds)
        {
            switch ([nextSound state])
            {
                case kMySoundStatePlaying:
                    if (nextSound.playProgress != nil)
                    {
                        [nextSound.playProgress setDoubleValue:[nextSound position]];
                        NSLog(@"         %@: %@ position: %f", [nextSound name], nextSound.playProgress, [nextSound position]);
                    }
                    break;
                case kMySoundStateQueued:
                case kMySoundStateReadyToPlay:
                case kMySoundStatePaused:
                case kMySoundStateFinishedPlaying:
                default:
                    break;
            }
        }
    }
    else
    {
        // hide window

        if ([window isVisible])
        {
            [window orderOut:self];
        }
    }

    // reload window table view

    [tableView reloadData];
}

init 期间,应用程序委托扫描/System/Library/Sounds/ 文件夹以获取该文件夹中的AIFF 声音文件列表,并创建一个sounds 数组来保存该文件夹中每个声音的声音对象。然后applicationDidFinishLaunching 方法开始按顺序播放列表中的第一个声音。

问题(您可以通过运行示例项目看到)是,不是仅更新当前正在播放的声音的顶部表格行,而是以下行的所有中的进度指示器似乎也在更新和闪烁。它的显示方式有些不一致(有时它们都在闪烁,有时它们像预期的那样都是空白的);但是当它们更新和闪烁时,进度指示器似乎与当前播放的声音大致对应。所以我很确定这个问题一定与我更新表格的方式有关;我只是不确定问题出在哪里或如何解决。

这是窗口外观的屏幕截图,可让您了解一下:

Table View Screen Shot

任何想法或指导将不胜感激!

【问题讨论】:

  • 仅供参考:Apple 的“以编程方式填充表视图”文档在这里:developer.apple.com/library/content/documentation/Cocoa/…
  • makeViewWithIdentifier:owner: "返回具有指定标识符的新视图或现有视图。"。所有声音都可以指向同一个控件。
  • 谢谢,@Willeke。我检查了一下,似乎每行的进度指示器确实不同。我在makeViewWithIdentifier 方法中添加了NSLog 语句,以输出对给定声音对象的NSProgressIndicator 的存储引用。这是一些示例输出: Basso [] Blow [] Bottle [] Frog []
  • 我收回最后的评论。我刚刚注意到有时进度指示器与您建议的相同:Tink: <NSProgressIndicator: 0x6000001e1f00> vs Submarine: <NSProgressIndicator: 0x6000001e1f00>,而其他次则不同:Sosumi: <NSProgressIndicator: 0x6380001e0700>。那么获取每一行的唯一单元格视图的正确方法是什么?
  • 您不需要唯一的单元格。始终在viewForTableColumn 中设置进度指示器的最大值和值。仅更新refreshWindow 中播放声音的行。删除声音时删除行。

标签: xcode cocoa nstableview flicker


【解决方案1】:

这是我所做的更改。

tableView:viewForTableColumn:row: 返回“显示指定行和列的视图”。进度条的值始终设置。

- (NSView *)tableView:(NSTableView *)thisTableView viewForTableColumn:(NSTableColumn *)thisTableColumn row:(NSInteger)thisRow
{
    SoundsTableCellView *cellView = nil;

    // get the table column identifier

    NSString *columnID = [thisTableColumn identifier];
    if ([columnID isEqualToString:@"sound column"])
    {
        // get the sound corresponding to the specified row (sounds array index)

        MySound *sound = [sounds objectAtIndex:thisRow];

        // get an existing cell from IB with our hard-coded identifier

        cellView = [thisTableView makeViewWithIdentifier:@"sound cell" owner:self];

        // display sound name

        [cellView.soundName setStringValue:[sound name]];

        // display sound details (source URL)

        NSString *details = [NSString stringWithFormat:@"%@", [sound sourceURL]];
        [cellView.soundDetails setStringValue:details];

        // update progress indicators

        //  [cellView.playProgress setUsesThreadedAnimation:NO];

        NSTimeInterval duration = [sound duration];
        NSTimeInterval position = [sound position];
        [cellView.playProgress setMaxValue:duration];
        [cellView.playProgress setDoubleValue:position];
    }

    // end updates

    //  [thisTableView endUpdates];

    return cellView;
}

refreshWindow 分为refreshProgressrefreshWindowrefreshProgress 刷新播放声音的行并在计时器上调用。

- (void)refreshProgress
{
    if ([sounds count] > 0)
    {
        [sounds enumerateObjectsUsingBlock:^(MySound *nextSound, NSUInteger rowNr, BOOL *stop)
        {
            switch ([nextSound state])
            {
                case kMySoundStatePlaying:
                    // refresh row
                    [tableView reloadDataForRowIndexes:[NSIndexSet indexSetWithIndex:rowNr]
                        columnIndexes:[NSIndexSet indexSetWithIndex:0]];
                    break;
                case kMySoundStateQueued:
                case kMySoundStateReadyToPlay:
                case kMySoundStatePaused:
                case kMySoundStateFinishedPlaying:
                default:
                    break;
            }
        }];
    }
}

refreshWindow 刷新窗口的大小和可见性,并在声音数量发生变化时调用。

- (void) refreshWindow
{
    if ([sounds count] > 0)
    {
        // show window if needed

        if ([window isVisible] == false)
        {
            [window makeKeyAndOrderFront:self];
        }

        // resize window to fit all sounds in the list if needed

        ... calculate new window frame

        }
    else
    {
        // hide window

        if ([window isVisible])
        {
            [window orderOut:self];
        }
    }
}

当一个声音被删除时,该行也被删除,所以其他行仍然显示相同的声音并且不需要更新。

- (void) soundFinishedPlaying:(MySound *)sound encounteredError:(NSError *)error
{
    if (error != NULL)
    {
        // display an error dialog box to the user

        [NSApp presentError:error];
    }
    else
    {
        // remove sound from array

        NSLog(@"deleting: [%@|%@]", [sound truncatedID], [sound name]);

        NSUInteger index = [sounds indexOfObject:sound];
        [sounds removeObject:sound];
        [tableView removeRowsAtIndexes:[NSIndexSet indexSetWithIndex:index] withAnimation:NSTableViewAnimationEffectNone];
    }

    // refresh window

    [self refreshWindow];

    // play the next sound in the queue

    [self play];
}

[tableView reloadData] 未被调用。 sound.playProgress 未使用。

【讨论】:

  • 啊哈。所以看起来答案是根本不调用[tableView reloadData],而是调用[tableView reloadDataForRowIndexes...],以及[tableView removeRowsAtIndexes...] 手动删除数据源数组中已删除项目的行。我仍然不完全理解为什么 reloadData 会导致表格视图中的all 进度指示器更新/闪烁,但是有更好的方法来更新它们很好。谢谢你给我指路! :)
  • makeViewWithIdentifier 重用视图,当不再使用视图时,将其放入可重用视图池中。当第一个声音被移除时,闪烁开始。当有 10 个视图和 9 个声音时,每个 reloadData 都会重用前 9 个视图,并在下一个 reloadData 上首先使用第 10 个视图。视图循环,viewForTableColumn 没有在每个视图中设置进度指示器的值。如果您将sound.namecellView.playProgress 的指针记录在viewForTableColumn 中,您可以看到这种情况。
  • 我现在明白了 - 明白了。谢谢,@Willeke! :)
猜你喜欢
  • 2023-03-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-08-25
  • 2010-09-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多