怎樣限制 Mac OS X 表格中編輯區中的輸入字數

怎樣限制 Mac OS X 表格中編輯區中的輸入字數

這篇文字來自 22 日晚間我在台北 Cocoaheads 活動中所分享的一個題目。

在講完之後,有些朋友覺得講得速度似乎有點快,而我回來之後看了一下自己的投影片,總感覺如果只是將投影片放在網路上,可能根本不知道我在說什麼,而更大的問題是,回來之後又做了一點小研究,發現有個地方講錯。看來,要在網路上分享這則故事,還是要換成比較詳盡的文字。

楔子

故事是這樣的。

一兩個月前,公司正在承接製作國內某知名輸入法廠商的 Mac OS X 版本的工作,專案的其中一環,是要製作一個使用表格輸入介面的自定字詞編輯工具-基本上就是在主視窗當中有一個表格,第一欄是自定詞的拆碼(或字根),第二欄則是這組拆碼所對應到的字詞。-當你在使用 Cocoa Framework 開發的時候,要使用表格介面,你當然會毫不猶疑的使用 NSTableView 物件。

因為這套輸入法的規則是,所有文字的打法最大只會有五碼,所以,在輸入或修改拆碼這個欄位的時候,如果使用者已經打了五個字元,就不應該繼續輸入。這件事情在 Windows 上面很簡單-如果你是寫一個 .Net 程式,你只要寫上一行,設上 DataGridViewTextBoxColumn.MaxInputLength (MSDN 文件)就好了,還可以在 IDE 中直接使用 GUI 設定;但是,在 NSTableView 的文件中,則是完全找不到對應的設定。

另外,客戶也要求,在按下 Enter 的時候要寫入資料,按下 ESC 按鍵的時候,則是要取消編輯狀態。如果是在 Mac OS X 10.5 上面,用 Interface Builder 拉好了一個 NSTableView,基本上這個 table view 在編輯狀態時,按下 Enter 就會完成編輯,這是我們要的,然而,如果按下 ESC 按鍵,預設卻是出現 Auto-complete 選單,幫你做些英文拼寫檢查之類的;至於在 10.4 上面,則是無論按下 Enter 或是 ESC,都不會理你…。

顯然在這個部分,也需要對 NSTableView 做一定的客製。

 

為了完成任務,第一步,就是先對 NSTableView 做一些小小的研究。

NSTableView

對於 NSTableView 的最基本的操作,當然就是要準備好一個給 NSTableView 使用的 delegate 與 data source 物件。

這邊簡單說一下 Cocoa 在處理物件之間通訊的常用技巧-Framework 所提供的絕大多數物件都會提供一個 delegate(當你撰寫自己的 Class 的時候,也會往往情不自禁的設計上去就是了),中文姑且稱之為「代理」,delegate 的用途就是-「當 A 把 B 設為代理人之後,每當 A 做了某件事情,A 就會告訴 B,B 應該要因為 A 做了這件事情所以應該去做另外一件事情。」比方說,我使用 一個 WebView 寫一個簡單的瀏覽器介面,我把這個 WebView 的 delegate 設定為 myController 這個物件,所以,當 WebView 發現自己載入網頁失敗的時候,就會告訴 myController 應該怎麼辦,myController 就會決定,嗯,我們應該跳出一個提示視窗。

Data source 則反之,是某個物件需要顯示資料的時候,去問這個設定為 data source 的物件,到底應該顯示什麼資料。以 NSTableView 來說,data source物件需要準備好對應的接口,告訴 NSTableView 在表格中的資料總共有幾列,哪一列哪一欄當中是怎樣型別-字串或是圖形-的資料。

而當使用者編輯完表格中某一格資料時,NSTableView 會呼叫一個 method,告訴 data source 在某一欄某一列中的資料已經有了變化-

tableView:setObjectValue:forTableColumn:row:

於是我在 NSTableDataSource 物件中寫了這樣的 code…

- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
{
	if ([[aTableColumn identifier] isEqualTo:@"column0"]) {
		NSString *s = (NSString *)anObject;
		if ([s length] > 5) {
			NSString *subString = [s substringToIndex:5];
			NSMutableDictionary *d = [_myArray objectAtIndex:rowIndex];
			[d setValue:subString forKey:@"key"];
               }
	}
}

專案一開始總是求快,就直接在這個地方,在收到改過的字串的時候,只留下前五個字元存起來便是。這種作法顯然不符合預期-我們想要的是打滿五個字就不能輸入,而不是容許使用者打出一大串字,在編輯完成的時候才裁掉字串。

要處理各種 Cocoa UI 物件-如目前正在討論的 NSTableView-當中的資料,除了提供 data source 物件之外,另外一種方式則是使用 Cocoa Binding,而在 Binding 的過程中,也可以使用 NSValueTransformer,但是 NSValueTransformer 所做的事情,一樣也是讓使用者先打好一大串文字,寫入的時候轉換,不是我們要的。

至於在處理 Enter 與 ESC 按鍵的部分,第一個想到的,是自己 subclass NSTableView,在自己的 subclass 中,覆蓋繼承自 NSResponder、用以接收鍵盤事件的 keyDown: method,像是這樣的程式-

- (void)keyDown:(NSEvent *)theEvent {
	if ([theEvent keyCode] == 53) { //ESC
		// ....
	}
	else if ([[theEvent charactersIgnoringModifiers] characterAtIndex:0] == 13) { //Enter
		// ....
	}
}

沒用。當表格處於編輯狀態的時候,事件根本沒有送到我們的 keyDown method 裡頭,只會在表格被選起來(周圍有一圈藍色發光的 focus ring 的那個狀態)卻不是編輯文字的時候有用。在做了這麼一點小研究之後,可以確定-我們要處理的不是 NSTableView 本身,而是用來編輯文字的那個編輯區本身那個物件,用蘋果的術語來說,那個編輯區叫做 Field Editor

Field Editor

Cocoa 是這樣設計的-就算你在同一個視窗當中,放置了好幾個型別為 NSTextField 的文字框,但是其實同一時間只會有一個文字框在負責文字輸入、編輯的工作-原因無他,就算有很多文字框,使用者一次也只會在一個文字框中打字,所以,在平常的狀況下,文字框其實只要負責把一段文字畫在螢幕上就夠了,等到使用者需要在這個文字框中打字,文字框才去跟 NSWindow 物件要求一個整個視窗共用的、具有編輯功能的文字輸入框,放在這個 NSTextField 物件的顯示範圍裡頭。這個為整個視窗中所有繼承自 NSControl 的物件所共用的編輯區,就叫做 Field Editor。

根據 header,Field Editor 是從 NSWindow 的 fieldEditor:forObject: method 中產生,回傳的型別是 NSText-當時因為年幼無知,年紀小不懂事,不知道讀文件應該要整個讀仔細,光看到 header 的型別是 NSText,就做了一個 NSText subclass,裡頭只實作一個keyDown: method,用來抽換 NSWindow 產生的 Field Editor。然而,Xcode 便如此告訴我-

All NSText methods must be implemented by subclasses. They should not call super.

耶-怎麼會一繼承下去,所有的 method 都要重新實作?而一看 NSText.h,嚇,裡頭定義的 method 總共大約七十來個,當場打消了 subclass NSText 的念頭,來研究看看 NSText 有什麼 delegate method,看看是不是可以在 delegate 物件這一端處理問題。

如前述,就因為讀文件不仔細,所以接下來這個嘗試也是失敗的。

Field Editor 的 delegate

看了看 textDidChange: 這個 method 應該就是我要的-這個 method 的用途是,當文字編輯區中的文字內容有改變時,就告訴 delegate 物件應該要做點事情;當一個 NSControl 物件處於編輯狀態的時候,Field Editor 的 delegate 會設為這個 NSControl,所以,我們來 subclass 一個 NSTableView,就可以收到文字是否有修改的訊息。

也有另外一種方法,NSControl 也有 delegate,而 NSControl 又會把自己收到的textDidChange: 以 controlTextDidChange: 傳遞出去,所以我們也可以在 NSControl 的 delegate 處理-就像是我不在我的戶籍地,但是里長可以先通知我父親,我父親再通知我去領消費券,而不需要我在我父親那邊留一筆任務是他一定要代替我領消費券(頂爛的比喻)。

寫了一小段程式,在收到文字編輯區的文字內容有變動的通知時,自動截掉第五個字元之後的內容-效果頗糟,原因是,使用者輸入文字時,輸入游標不一定是在文字的結尾,所以當原本的內容是「12345」時,我將游標移動到 2 與 3 之間,再打一個 A,原本預期的是不會有影響,但是這個作法卻是內容變成「12A34」…。

重新釐清目標-不是在輸入後才改變輸入的結果,而是在輸入的同時就決定要不要處理鍵盤事件。

但是 NSText 的 delegate method 卻解決了 Enter 鍵與 ESC 鍵的問題。程式碼如下:

- (BOOL)control:(NSControl*)control textView:(NSTextView*)textView doCommandBySelector:(SEL)commandSelector
{	
	if ([[textView delegate] isKindOfClass:[NSTableView class]]) { // 型別為 NSTableView
		if (commandSelector == @selector(insertNewline:)) { // Enter
			[[textView window] endEditingFor:textView];
			[[textView window] makeFirstResponder:[textView delegate]];
			return YES;
		} 
		else if (commandSelector == @selector(cancelOperation:)) { // ESC
			[[textView delegate] abortEditing];
			[[textView window] makeFirstResponder:[textView delegate]];	
            return YES;
		}	
	}
	if ([textView respondsToSelector:commandSelector]) {
        [textView performSelector:commandSelector withObject:nil];
        return YES;
    }
    return NO;
 
}

附註:我在這邊只有檢查發生文字有改變的 NSControl 物件是不是 NSTableView,如果您要拿這段程式去用,您還是需要檢查是不是你要的那個物件,畢竟您有可能把很多個 NSTableView 的 delegate 指定到相同的物件上。

[NSWindow sendEvent:(NSEvent *)event]

一想到要解決問題是要讓事件不要送出,一時之間又搞不清楚怎樣 subclass NSText 物件收事件,當時想到的方法就是-我要攔截事件,卻搞不定收事件的這一段,那,就從發事件的那一端著手。

Mac OS X 在收到各種(鍵盤、滑鼠)事件後,處理的順序為-先去尋找哪一個是目前使用者正在用的應用程式,把事件送過去,應用程式再判斷使用者正在使用那個視窗,把事件送到視窗(NSWindow)去,視窗再判斷哪一個是使用者正在使用的 UI 元件如按鈕、文字框等,再把事件送過去。我們想要在 NSTableView 收到事件前攔截事件,那就是在視窗將事件送過去時攔截,NSWindow 負責傳遞事件的 method 就叫做 sendEvent:,的確是非常容易顧名思義。

一試之下居然可以了…。

- (void)sendEvent:(NSEvent *)event
{
	if ([event type] == NSKeyDown) {
		if (isprint([[event charactersIgnoringModifiers] characterAtIndex:0])) { // 英數字母
			NSText *text = [self fieldEditor:YES forObject:nil];
			if ([text delegate] != nil &&  // 有人正在用 FieldEditor
				[[text delegate] isKindOfClass:[NSTableView class]]) { // Field Editor 正被 NSTableView 使用
				NSTableView *tableView = [text delegate]; // 我習慣重新宣告型別
				if ([tableView editedColumn] == 0 && // 在編是我們想要的那一列
					[[text string] length] >= 5 && // 已經有五個字
					![text selectedRange].length) { // 沒有選擇範圍
					NSBeep(); // 發出錯誤聲
					return; // 不處理
				}
			}
		}
	}
	[super sendEvent:event];
}

如前所述,這邊我也只是單純判斷在使用 Field Editor 的物件的型別而已;另外就是在判斷文字編輯區的內容時,還要加上「有沒有選擇範圍」這個條件-就算打滿了,但是有一部分文字被選起來,還是要可以送出鍵盤按鍵,當你打了「ABCDE」,全選,之後再打一個「A」,預期效果不該是不出字,而是全部內容換成「A」。

對 Enter 與 ESC 的處理,也可以用相同的作法。

- (void)sendEvent:(NSEvent *)event
{
	if ([event type] == NSKeyDown) {
		if ([event keyCode] == 53) { // ESC
			NSText *text = [self fieldEditor:YES forObject:nil];
			if ([text delegate] != nil &&  // 有人正在用 FieldEditor
				[[text delegate] isKindOfClass:[NSTableView class]]) { // FieldEditor 正被 NSTableView 使用
				NSTableView *tableView = [text delegate];
				[tableView abortEditing]; // 取消編輯
				[self makeFirstResponder:tableView]; // 把焦點從 FieldEditor 轉回 TableView
				return;
			}
		}
	}
	[super sendEvent:event];
}

既然能動,產品就做出來了。所以可以回頭來看-那個不能呼叫 super 的 NSText subclass 是怎麼一回事啊?

結語

其實在蘋果的在 NSText 與 NSWindow 的文件就說得很清楚,雖然fieldEditor:forObject: 回傳的型別是 NSText,但是 Field Editor 的型別卻是繼承自 NSText 的 NSTextView,所以,之所以直接繼承自 NSText 的型別需要自己實作所有的 method,就是因為 NSText 就只是抽象定義,沒有實作,所有實作都在 NSTextView 嘛…。

所以要用自己的 subclass 收 keyDown: 事件,就是不要繼承 NSText,改成繼承 NSTextView 就好了(嘆)。但是想想這樣寫好像也沒有比較好-在 NSWindow 的 sendEvent: 那端改,只要 subclass NSWindow 就好,至於抽換 Field Editor,一方面要 subclass NSWindow,另一方面也要 subclass NSTextView,反而要生出兩個 subclass,對於我這種懶人而言,好像還更麻煩。

但不管用什麼作法,都可以得到以下結論-為什麼我要改一個表格的行為,不管怎樣,都要從視窗物件改起啊?更況且,在 Windows 上面只要寫一行,在 Mac OS X 上面,卻變成一則不太想要統計字數的故事。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


我正在用TitaniumDeveloper编写一个应用程序,它允许我使用Javascript,PHP,Ruby和Python.它为API提供了一些可能需要的标准功能,但缺少的是全局事件.现在我想将全局热键分配给我的应用程序并且几乎没有任何问题.现在我只针对MAC,但无法找到任何Python或Ruby的解决方案.我找到了Coc
我的问题是当我尝试从UIWebView中调用我的AngularJS应用程序中存在的javascript函数时,该函数无法识别.当我在典型的html结构中调用该函数时,该函数被识别为预期的.示例如下:Objective-C的:-(void)viewDidLoad{[superviewDidLoad];//CODEGOESHERE_webView.d
我想获取在我的Mac上运行的所有前台应用程序的应用程序图标.我已经使用ProcessManagerAPI迭代所有应用程序.我已经确定在processMode中设置了没有modeBackgroundOnly标志的任何进程(从GetProcessInformation()中检索)是一个“前台”应用程序,并显示在任务切换器窗口中.我只需要
我是一名PHP开发人员,我使用MVC模式和面向对象的代码.我真的想为iPhone编写应用程序,但要做到这一点我需要了解Cocoa,但要做到这一点我需要了解Objective-C2.0,但要做到这一点我需要知道C,为此我需要了解编译语言(与解释相关).我应该从哪里开始?我真的需要从简单的旧“C”开始,正
OSX中的SetTimer在Windows中是否有任何等效功能?我正在使用C.所以我正在为一些软件编写一个插件,我需要定期调用一个函数.在Windows上,我只是将函数的地址传递给SetTimer(),它将以给定的间隔调用.在OSX上有一个简单的方法吗?它应该尽可能简约.我并没有在网上找到任何不花哨的东西
我不确定引擎盖下到底发生了什么,但这是我的设置,示例代码和问题:建立:>雪豹(10.6.8)>Python2.7.2(由EPD7.1-2提供)>iPython0.11(由EPD7.1-2提供)>matplotlib(由EPD7.1-2提供)示例代码:importnumpyasnpimportpylabasplx=np.random.normal(size=(1000,))pl.plot
我正在使用FoundationFramework在Objective-C(在xCode中)编写命令行工具.我必须使用Objective-C,因为我需要取消归档以前由NSKeyedArchiver归档的对象.我的问题是,我想知道我现在是否可以在我的Linux网络服务器上使用这个编译过的应用程序.我不确定是否会出现运行时问题,或者可
使用cocoapods,我们首先了解一下rvm、gem、ruby。rvm和brew一样,但是rvm是专门管理ruby的版本控制的。rvmlistknown罗列出ruby版本rvminstall版本号   可以指定更新ruby版本而gem是包管理gemsource-l查看ruby源gemsource-rhttps://xxxxxxxx移除ruby源gemsou
我有一个包含WebView的Cocoa应用程序.由于应用程序已安装客户群,我的目标是10.4SDK.(即我不能要求Leopard.)我有两个文件:index.html和data.js.在运行时,为了响应用户输入,我通常会使用应用程序中的当前数据填充data.js文件.(data.js文件由body.html上的index.html文件用于填充
如何禁用NSMenuItem?我点击后尝试禁用NSMenuItem.操作(注销)正确处理单击.我尝试通过以下两种方式将Enabled属性更改为false:partialvoidLogout(AppKit.NSMenuItemsender){sender.Enabled=false;}和partialvoidLogout(AppKit.NSMenuItemsender){LogoutI
我在想,创建一个基本上只是一个带Web视图的界面的Cocoa应用程序是否可行?做这样的事情会有一些严重的限制吗?如果它“可行”,那是否也意味着你可以为Windows应用程序做同样的事情?解决方法:当然可以创建一个只是一个Cocoa窗口的应用程序,里面有一个Web视图.这是否可以被称为“可可应
原文链接:http://www.cnblogs.com/simonshi2012/archive/2012/10/08/2715464.htmlFrom:http://www.idev101.com/code/Cocoa/Notifications.htmlNotificationsareanincrediblyusefulwaytosendmessages(anddata)betweenobjectsthatotherwi
如果不手动编写GNUmake文件,是否存在可以理解Xcode项目的任何工具,并且可以直接针对GNUstep构建它们,从而生成Linux可执行文件,从而简化(略微)保持项目在Cocoa/Mac和GNUstep/Linux下运行所需的工作?基本上,是否有适用于Linux的xcodebuild样式应用程序?几个星期前我看了pbtomake
我正在将页面加载到WebView中.该页面有这个小测试Javascript:<scripttype="text/javascript">functiontest(parametr){$('#testspan').html(parametr);}varbdu=(function(){return{secondtest:function(parametr){$('#testspan&#039
我正在尝试使用NSAppleScript从Objective-C执行一些AppleScript…但是,我正在尝试的代码是Yosemite中用于自动化的新JavaScript.它在运行时似乎没有做任何事情,但是,正常的AppleScript工作正常.[NSAppactivateIgnoringOtherApps:YES];NSAppleScript*scriptObject=[[NSApple
链接:https://pan.baidu.com/s/14_im7AmZ2Kz3qzrqIjLlAg           vjut相关文章Python与Tkinter编程ProgrammingPython(python编程)python基础教程(第二版)深入浅出PythonPython源码剖析Python核心编程(第3版)图书信息作者:Kochan,StephenG.出
我正在实现SWTJava应用程序的OSX版本的视图,并希望在我的SWT树中使用NSOutlineView提供的“源列表”选项.我通过将此代码添加到#createHandle()方法来破解我自己的Tree.class版本来实现这一点:longNSTableViewSelectionHighlightStyleSourceList=1;longhi=OS.sel_regist
我的Cocoa应用程序需要使用easy_install在用户系统上安装Python命令行工具.理想情况下,我想将一个bash文件与我的应用程序捆绑在一起然后运行.但据我所知这是不可能的,因为软件包安装在Python的“site-packages”目录中.有没有办法创建这些文件的“包”?如果没有,我应该如何运行ea
摘要: 文章工具 收藏 投票评分 发表评论 复制链接 Swing 是设计桌面应用程序的一个功能非常强大工具包,但Swing因为曾经的不足常常遭到后人的诟病.常常听到旁人议论纷纷,”Swing 运行太慢了!”,”Swing 界面太丑嘞”,甚至就是说”Swing 简直食之无味”. 从Swing被提出到现在,已是十年光景,Swing早已不是昔日一无是处的Swing了. Chris Adamson 和我写
苹果的开发:   我对于Linux/Unix的开发也是一窍不通,只知道可以用Java.不过接触了苹果过后,确实发现,世界上确实还有那么一帮人,只用苹果,不用PC的.对于苹果的开发,我也一点都不清楚,以下是师兄们整理出来的网站. http://www.chezmark.com/osx/    共享软件精选 http://www.macosxapps.com/    分类明了,更新及时的一个重要Mac