【问题标题】:Arrow keys with NSTableView带有 NSTableView 的箭头键
【发布时间】:2023-03-18 23:26:01
【问题描述】:

是否可以使用箭头键和 enter/tab 在 NSTableView 周围导航 NSTableView 的可编辑单元格?例如,我想让它更像电子表格。

这个应用程序的用户应该编辑相当多的单元格(但不是全部),我认为如果他们不必双击每个单元格,这样做会更容易。

【问题讨论】:

    标签: objective-c cocoa macos


    【解决方案1】:

    在 Sequel Pro 中,我们使用了不同的(在我看来更简单)的方法:我们在 TableView 的委托中实现了control:textView:doCommandBySelector:。这种方法很难找到——可以在NSControlTextEditingDelegate 协议参考中找到。 (记住 NSTableView 是 NSControl 的子类)

    长话短说,这就是我们的想法(我们没有覆盖左/右箭头键,因为它们用于在单元格内导航。我们使用 Tab 向左/向右移动)

    请注意,这只是 Sequel Pro 源代码中的一个 sn-p,不能按原样工作

    - (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command
    {
        NSUInteger row, column;
    
        row = [tableView editedRow];
        column = [tableView editedColumn];
    
        // Trap down arrow key
        if (  [textView methodForSelector:command] == [textView methodForSelector:@selector(moveDown:)] )
        {
            NSUInteger newRow = row+1;
            if (newRow>=numRows) return TRUE; //check if we're already at the end of the list
            if (column>= numColumns) return TRUE; //the column count could change
    
            [tableContentView selectRowIndexes:[NSIndexSet indexSetWithIndex:newRow] byExtendingSelection:NO];
            [tableContentView editColumn:column row:newRow withEvent:nil select:YES];
            return TRUE;
        }
    
        // Trap up arrow key
        else if (  [textView methodForSelector:command] == [textView methodForSelector:@selector(moveUp:)] )
        {
            if (row==0) return TRUE; //already at the beginning of the list
            NSUInteger newRow = row-1;
    
            if (newRow>=numRows) return TRUE;
            if (column>= numColumns) return TRUE;
    
            [tableContentView selectRowIndexes:[NSIndexSet indexSetWithIndex:newRow] byExtendingSelection:NO];
            [tableContentView editColumn:column row:newRow withEvent:nil select:YES];
            return TRUE;
        }
    

    【讨论】:

    • 这看起来真的很强大,但如果你能更好地解释你在做什么/如何做,它会很有帮助。例如我试过了,它从来没有为我捕获向上/向下箭头键 - 尽管它确实捕获了用户在单元格上点击 Return 并编辑该单元格?
    • 这仅在编辑单元格时有效。当您单击 NSTableView 的单元格时,窗口字段编辑器将放置在单元格上方,您可以编辑文本。当您在字段编辑器中编辑文本时,上述消息将发送给表视图的委托。我们使用的委托方法的文档是here
    【解决方案2】:

    这并不容易,但我设法做到了,而无需使用 RRSpreadSheet 甚至其他控件。这是你必须做的:

    1. 创建NSTextView 的子类,这将是字段编辑器。在本例中,将使用名称 MyFieldEditorClass,而 myFieldEditor 将引用此类的一个实例。

    2. MyFieldEditorClass 添加一个名为“- (void) setLastKnownColumn:(unsigned)aCol andRow:(unsigned) aRow”或类似名称的方法,并将两个输入参数值保存在某处。

    3. 添加另一个名为“setTableView:”的方法并将 NSTableView 对象保存在某处,或者除非有其他方法可以从字段编辑器中获取 NSTableView 对象,否则请使用该方法。

    4. 添加另一个名为- (void) keyDown:(NSEvent *) event 的方法。这实际上覆盖了NSResponderkeyDown:。源代码应该是(注意 StackOverflow 的 MarkDown 正在将 <> 更改为 <>):

      - (void) keyDown:(NSEvent *) event
      {
          unsigned newRow = row, newCol = column;
          switch ([event keyCode])
          {
              case 126: // Up
                  if (row)
                  newRow = row - 1;
                  break;
      
              case 125: // Down
                  if (row < [theTable numberOfRows] - 1)
                      newRow = row + 1;
                  break;
      
              case 123: // Left
                  if (column > 1)
                      newCol = column - 1;
                  break;
      
              case 124: // Right
                  if (column < [theTable numberOfColumns] - 1)
                      newCol = column + 1;
                  break;
      
              default:
                  [super keyDown:event];
                  return;
          }
      
          [theTable selectRow:newRow byExtendingSelection:NO];
          [theTable editColumn:newCol row:newRow withEvent:nil select:YES];
          row = newRow;
          column = newCol;
      }
      
    5. 给 nib 中的 NSTableView 一个委托,并在委托中添加方法:

      - (BOOL) tableView:(NSTableView *)aTableView shouldEditColumn:(NSTableColumn *) aCol row:aRow
      {
          if ([aTableView isEqual:TheTableViewYouWantToChangeBehaviour])
              [myFieldEditor setLastKnownColumn:[[aTableView tableColumns] indexOfObject:aCol] andRow:aRow];
          return YES;
      }
      
    6. 最后,给 Table View 的主窗口一个委托并添加方法:

      - (id) windowWillReturnFieldEditor:(NSWindow *) aWindow toObject:(id) anObject
      {
          if ([anObject isEqual:TheTableViewYouWantToChangeBehaviour])
          {
              if (!myFieldEditor)
              {
                  myFieldEditor = [[MyFieldEditorClass alloc] init];
                  [myFieldEditor setTableView:anObject];
              }
              return myFieldEditor;
          }
          else
          {
              return nil;
          }
      }
      

    运行程序并试一试!

    【讨论】:

      【解决方案3】:

      与其强迫NSTableView 做一些不是为它设计的事情,不如考虑使用为此目的而设计的东西。我有一个开源电子表格控件,它可以满足您的需求,或者您至少可以扩展它来满足您的需求:MBTableGrid

      【讨论】:

      • 在 MBTableGrid 上的出色工作(以及慷慨的许可证),但这比我为满足我的需要所做的代码要多得多。为您在 MBTableGrid 中付出的努力 +1。
      【解决方案4】:

      我想在这里回复答案,但回复按钮似乎不见了,所以当我真的只想问一个关于回复的问题时,我不得不证明答案。

      无论如何,我已经看到了一些关于覆盖表视图的 -keyDown 事件的答案,这些答案说是对 TableView 进行子类化,但根据我目前阅读的每一本 Objective-C 书籍和几个 Apple 培训视频,你如果曾经子类化核心类之一,则应该很少。事实上,他们每个人都指出 C 程序员对子类化很着迷,而这不是 Objective-C 的工作方式。 Objective-C 完全是关于助手和委托而不是子类化。

      那么,我是否应该忽略任何对子类的回应,因为这似乎与 Objective-C 的规则直接矛盾?

      --- 编辑---

      我发现了一些无需子类化 NSTableView 即可工作的东西。虽然我确实将继承从 NSObject 到 NSResponder 的链上移了一个档次,但我并没有完全继承 NSTableView。我只是添加了覆盖 keyDown 事件的功能。

      我将用作委托的类继承自 NSResponder 而不是 NSObject,并在 awakeFromNib 中将 nextResponder 设置为该类。然后我能够使用 keydown 事件来捕获按键。我当然连接了 IBOutlet 并在 Interface Builder 中设置了委托。

      这是我的代码,显示键的捕获所需的最低要求:

      头文件

      //  AppController.h
      
      #import <Cocoa/Cocoa.h>
      
      @interface AppController : NSResponder {
      
          IBOutlet NSTableView *toDoListView;
          NSMutableArray *toDoArray;
      }
      
      -(int)numberOfRowsInTableView:(NSTableView *)aTableView;
      
      -(id)tableView:(NSTableView *)tableView
      objectValueForTableColumn:(NSTableColumn *)aTableColumn
                 row:(int)rowIndex;
      
      @end
      

      这是 m 文件。

      //  AppController.m
      #import "AppController.h"
      
      @implementation AppController
      
      -(id)init
      {
          [super init];
          toDoArray = [[NSMutableArray alloc] init];
          return self;
      }
      
      -(void)dealloc
      {
          [toDoArray release];
          toDoArray = nil;
          [super dealloc];
      }
      
      -(void)awakeFromNib
      {
          [toDoListView setNextResponder:self];
      }
      
      -(int)numberOfRowsInTableView:(NSTableView *)aTableView
      {
          return [toDoArray count];
      }
      
      -(id)tableView:(NSTableView *)tableView
          objectValueForTableColumn:(NSTableColumn *)aTableColumn
                                row:(int)rowIndex
      {
          NSString *value = [toDoArray objectAtIndex:rowIndex];
          return value;
      }
      
      - (void)keyDown:(NSEvent *)theEvent
      {
          //NSLog(@"key pressed: %@", theEvent);
          if (theEvent.keyCode == 51 || theEvent.keyCode == 117)
          {
              [toDoArray removeObjectAtIndex:[toDoListView selectedRow]];
              [toDoListView reloadData];
          }
      }
      @end
      

      【讨论】:

      • 有些类你不应该子类化,但有很多核心(即 Foundation 和 AppKit)类是你应该必须的子类。 NSObject 是最明显的一个。
      • 我学到了一些东西,距离成为 Objective-C 的新手还有很长的路要走,但我现在更加确定我的解决方案是更好的解决方案。当我只需要捕获 keydown 事件时,无需执行整个 NSTableView 类。从我仅有的一点点知识来看,这似乎更符合 Apple 关于如何用 Objective-C 编写代码的愿景,并且在处理了越来越多的 Apple API 之后,我真的开始尊重他们的标准;尤其是在来自 Windows 世界之后,他们自己的代码通常不遵循自己的标准。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-06-25
      • 2011-11-22
      • 1970-01-01
      • 1970-01-01
      • 2018-01-16
      • 1970-01-01
      • 2011-01-11
      相关资源
      最近更新 更多