精华内容
下载资源
问答
  • 人工智能时代,所需要了解人工智能的基本常识

    万次阅读 多人点赞 2018-12-10 22:49:44
     Facebook聘用了人工智能学界泰斗YannLeCun来创建自己的人工智能实验室,期望在该领域获得重大突破。  牛津大学的研究人员发表了一篇报告表明,美国大约47%的工作因为机器认知技术自动化而得岌岌可危。  ...

      国内对于人工智能的讨论大多是不成体系的碎片式,很难从中深入了解人工智能的发展脉络和技术体系,也很难有实际借鉴意义。人工智能的历史、核心技术和应用情况进行了详细说明,尤其是其中重要的认知技术。这份报告将有助于我们对人工智能和认知技术进行深入了解,也有助于各行业的公司考量人工智能应用的实际价值。

      一、概述

      近几年各界对人工智能的兴趣激增,自2011年以来,开发与人工智能相关的产品和技术并使之商业化的公司已获得超过总计20亿美元的风险投资,而科技巨头更是投资数十亿美元收购那些人工智能初创公司。相关报道铺天盖地,而巨额投资、计算机导致失业等问题也开始浮现,计算机比人更加聪明并有可能威胁到人类生存这类论断更是被媒体四处引用并引发广泛关注。

      IBM承诺拨出10亿美元来使他们的认知计算平台Watson商业化。

      谷歌在最近几年里的投资主要集中在人工智能领域,比如收购了8个机器人公司和1个机器学习公司。

      Facebook聘用了人工智能学界泰斗YannLeCun来创建自己的人工智能实验室,期望在该领域获得重大突破。

      牛津大学的研究人员发表了一篇报告表明,美国大约47%的工作因为机器认知技术自动化而变得岌岌可危。

      纽约时报畅销书《TheSecondMachineAge》论断,数字科技和人工智能带来巨大积极改变的时代已经到来,但是随之而来的也有引发大量失业等负面效应。

      硅谷创业家ElonMusk则通过不断投资的方式来保持对人工智能的关注。他甚至认为人工智能的危险性超过核武器。

      著名理论物理学家StephenHawking认为,如果成功创造出人工智能则意味着人类历史的终结,“除非我们知道如何规避风险。”

      即便有如此多炒作,但人工智能领域却也不乏显著的商业行为,这些活动已经或者即将对各个行业和组织产生影响。商业领袖需要透彻理解人工智能的含义以及发展趋势。

      二、人工智能与认知科技

      揭秘人工智能的首要步骤就是定义专业术语,勾勒历史,同时描述基础性的核心技术。

      1、人工智能的定义

      人工智能领域苦于存在多种概念和定义,有的太过有的则不够。作为该领域创始人之一的NilsNilsson先生写到:“人工智能缺乏通用的定义。”一本如今已经修订三版的权威性人工智能教科书给出了八项定义,但书中并没有透露其作者究竟倾向于哪种定义。对于我们来说,一种实用的定义即为——人工智能是对计算机系统如何能够履行那些只有依靠人类智慧才能完成的任务的理论研究。例如,视觉感知、语音识别、在不确定条件下做出决策、学习、还有语言翻译等。比起研究人类如何进行思维活动,从人类能够完成的任务角度对人工智能进行定义,而非人类如何思考,在当今时代能够让我们绕开神经机制层面对智慧进行确切定义从而直接探讨它的实际应用。值得一提的是,随着计算机为解决新任务挑战而升级换代并推而广之,人们对那些所谓需要依靠人类智慧才能解决的任务的定义门槛也越来越高。所以,人工智能的定义随着时间而演变,这一现象称之为“人工智能效应”,概括起来就是“人工智能就是要实现所有目前还无法不借助人类智慧才能实现的任务的集合。”

      2、人工智能的历史

      人工智能并不是一个新名词。实际上,这个领域在20世纪50年代就已经开始启动,这段探索的历史被称为“喧嚣与渴望、挫折与失望交替出现的时代”——最近给出的一个较为恰当的评价。

      20世纪50年代明确了人工智能要模拟人类智慧这一大胆目标,从此研究人员开展了一系列贯穿20世纪60年代并延续到70年代的研究项目,这些项目表明,计算机能够完成一系列所本只属于人类能力范畴之内的任务,例如证明定理、求解微积分、通过规划来响应命令、履行物理动作,甚至是模拟心理学家、谱曲这样的活动。

      但是,过分简单的算法、匮乏的难以应对不确定环境(这种情形在生活中无处不在)的理论,以及计算能力的限制严重阻碍了我们使用人工智能来解决更加困难和多样的问题。伴随着对缺乏继续努力的失望,人工智能于20世纪70年代中期逐渐淡出公众视野。

      20世纪80年代早期,日本发起了一个项目,旨在开发一种在人工智能领域处于领先的计算机结构。西方开始担心会在这个领域输给日本,这种焦虑促使他们决定重新开始对人工智能的投资。20世纪80年代已经出现了人工智能技术产品的商业供应商,其中一些已经上市,例如Intellicorp、Symbolics、和Teknowledge。

      20世纪80年代末,几乎一半的“财富500强”都在开发或使用“专家系统”,这是一项通过对人类专家的问题求解能力进行建模,来模拟人类专家解决该领域问题的人工智能技术。

      对于专家系统潜力的过高希望彻底掩盖了它本身的局限性,包括明显缺乏常识、难以捕捉专家的隐性知识、建造和维护大型系统这项工作的复杂性和成本,当这一点被越来越多的人所认识到时,人工智能研究再一次脱离轨道。

      20世纪90年代在人工智能领域的技术成果始终处于低潮,成果寥寥。反而是神经网络、遗传算法等科技得到了新的关注,这一方面是因为这些技术避免了专家系统的若干限制,另一方面是因为新算法让它们运行起来更加高效。

      神经网络的设计受到了大脑结构的启发。遗传算法的机制是,首先迭代生成备选解决方案,然后剔除最差方案,最后通过引入随机变量来产生新的解决方案,从而“进化”出解决问题的最佳方案。

      3、人工智能进步的催化剂

      截止到21世纪前10年的后期,出现了一系列复兴人工智能研究进程的要素,尤其是一些核心技术。下面将对这些重要的因素和技术进行详细说明。

      1)摩尔定律

      在价格、体积不变的条件下,计算机的计算能力可以不断增长。这就是被人们所熟知的摩尔定律,它以Intel共同创办人GordonMoore命名。GordonMoore从各种形式的计算中获利,包括人工智能研究人员使用的计算类型。数年以前,先进的系统设计只能在理论上成立但无法实现,因为它所需要的计算机资源过于昂贵或者计算机无法胜任。今天,我们已经拥有了实现这些设计所需要的计算资源。举个梦幻般的例子,现在最新一代微处理器的性能是1971年第一代单片机的400万倍。

      2)大数据

      得益于互联网、社交媒体、移动设备和廉价的传感器,这个世界产生的数据量急剧增加。随着对这些数据的价值的不断认识,用来管理和分析数据的新技术也得到了发展。大数据是人工智能发展的助推剂,这是因为有些人工智能技术使用统计模型来进行数据的概率推算,比如图像、文本或者语音,通过把这些模型暴露在数据的海洋中,使它们得到不断优化,或者称之为“训练”——现在这样的条件随处可得。

      3)互联网和云计算

      和大数据现象紧密相关,互联网和云计算可以被认为是人工智能基石有两个原因,第一,它们可以让所有联网的计算机设备都能获得海量数据。这些数据是人们推进人工智能研发所需要的,因此它可以促进人工智能的发展。第二,它们为人们提供了一种可行的合作方式——有时显式有时隐式——来帮助人工智能系统进行训练。比如,有些研究人员使用类似MechanicalTurk这样基于云计算的众包服务来雇佣成千上万的人来描绘数字图像。这就使得图像识别算法可以从这些描绘中进行学习。谷歌翻译通过分析用户的反馈以及使用者的无偿贡献来提高它自动翻译的质量。

      4)新算法

      算法是解决一个设计程序或完成任务的路径方法。最近几年,新算法的发展极大提高了机器学习的能力,这些算法本身很重要,同时也是其他技术的推动者,比如计算机视觉(这项科技将会在后文描述)。机器学习算法目前被开源使用,这种情形将促成更大进步,因为在开源环境下开发人员可以补足和增强彼此的工作。

      4、认知技术

      我们将区分人工智能领域和由此延伸的各项技术。大众媒体将人工智能刻画为跟人一样聪明的或比人更聪明的计算机的来临。而各项技术则在以往只有人能做到的特定任务上面表现得越来越好。我们称这些技术为认知技术(下图),认知技术是人工智能领域的产物,它们能完成以往只有人能够完成的任务。而它们正是商业和公共部门的领导者应该关注的。下面我们将介绍几个最重要的认知技术,它们正被广泛采纳并进展迅速,也获得大量投资。

      一文了解人工智能的基本常识

      1)计算机视觉

      是指计算机从图像中识别出物体、场景和活动的能力。计算机视觉技术运用由图像处理操作及其他技术所组成的序列来将图像分析任务分解为便于管理的小块任务。比如,一些技术能够从图像中检测到物体的边缘及纹理。分类技术可被用作确定识别到的特征是否能够代表系统已知的一类物体。

      计算机视觉有着广泛应用。其中包括,医疗成像分析被用来提高疾病的预测、诊断和治疗;人脸识别被Facebook用来自动识别照片里的人物;在安防及监控领域被用来指认嫌疑人;在购物方面,消费者现在可以用智能手机拍摄下产品以获得更多购买选择。

      机器视觉作为一个相关学科,泛指在工业自动化领域的视觉应用。在这些应用里,计算机在高度受限的工厂环境里识别诸如生产零件一类的物体,因此相对于寻求在非受限环境里操作的计算机视觉来说目标更为简单。计算机视觉是一个正在进行中的研究,而机器视觉则是“已经解决的问题”,是系统工程方面的课题而非研究层面的课题。因为应用范围的持续扩大,计算机视觉领域的初创公司自2011年起已经吸引了数亿美元的风投资本。

      2)机器学习

      指的是计算机系统无需遵照显式的程序指令而只是依靠暴露在数据中来提升自身性能的能力。其核心在于,机器学习是从数据中自动发现模式,模式一旦被发现便可用于做预测。比如,给予机器学习系统一个关于交易时间、商家、地点、价格及交易是否正当等信用卡交易信息的数据库,系统就会学习到可用来预测信用卡欺诈的模式。处理的交易数据越多,预测就会越好。

      机器学习的应用范围非常广泛,针对那些产生庞大数据的活动,它几乎拥有改进一切性能的潜力。除了欺诈甄别之外,这些活动还包括销售预测、库存管理、石油和天然气勘探、以及公共卫生。机器学习技术在其他的认知技术领域也扮演着重要角色,比如计算机视觉,它能在海量图像中通过不断训练和改进视觉模型来提高其识别对象的能力。现如今,机器学习已经成为认知技术中最炙手可热的研究领域之一,在2011-2014年中这段时间内就已吸引了近十亿美元的风险投资。谷歌也在2014年斥资4亿美金收购Deepmind这家研究机器学习技术的公司。

      3)自然语言处理

      是指计算机拥有的人类般文本处理的能力,比如,从文本中提取意义,甚至从那些可读的、风格自然、语法正确的文本中自主解读出含义。一个自然语言处理系统并不了解人类处理文本的方式,但是它却可以用非常复杂与成熟的手段巧妙处理文本,例如自动识别一份文档中所有被提及的人与地点;识别文档的核心议题;或者在一堆仅人类可读的合同中,将各种条款与条件提取出来并制作成表。以上这些任务通过传统的文本处理软件根本不可能完成,后者仅能针对简单的文本匹配与模式进行操作。请思考一个老生常谈的例子,它可以体现自然语言处理面临的一个挑战。在句子“光阴似箭(Timeflieslikeanarrow)”中每一个单词的意义看起来都很清晰,直到系统遇到这样的句子“果蝇喜欢香蕉(Fruitflieslikeabanana)”,用“水果(fruit)”替代了“时间(time)”,并用“香蕉(banana)”替代“箭(arrow)”,就改变了“飞逝/飞着的(like)”与“像/喜欢(like)”这两个单词的意思。

      自然语言处理,像计算机视觉技术一样,将各种有助于实现目标的多种技术进行了融合。建立语言模型来预测语言表达的概率分布,举例来说,就是某一串给定字符或单词表达某一特定语义的最大可能性。选定的特征可以和文中的某些元素结合来识别一段文字,通过识别这些元素可以把某类文字同其他文字区别开来,比如垃圾邮件同正常邮件。以机器学习为驱动的分类方法将成为筛选的标准,用来决定一封邮件是否属于垃圾邮件。

      因为语境对于理解“timeflies(时光飞逝)”和“fruitflies(果蝇)”的区别是如此重要,所以自然语言处理技术的实际应用领域相对较窄,这些领域包括分析顾客对某项特定产品和服务的反馈、自动发现民事诉讼或政府调查中的某些含义、以及自动书写诸如企业营收和体育运动的公式化范文等。

      4)机器人技术

      将机器视觉、自动规划等认知技术整合至极小却高性能的传感器、致动器、以及设计巧妙的硬件中,这就催生了新一代的机器人,它有能力与人类一起工作,能在各种未知环境中灵活处理不同的任务。例如无人机,还有可以在车间为人类分担工作的“cobots”,还包括那些从玩具到家务助手的消费类产品。

      5)语音识别技术

      主要是关注自动且准确的转录人类的语音。该技术必须面对一些与自然语言处理类似的问题,在不同口音的处理、背景噪音、区分同音异形异义词(“buy”和“by”听起来是一样的)方面存在一些困难,同时还需要具有跟上正常语速的工作速度。语音识别系统使用一些与自然语言处理系统相同的技术,再辅以其他技术,比如描述声音和其出现在特定序列和语言中概率的声学模型等。语音识别的主要应用包括医疗听写、语音书写、电脑系统声控、电话客服等。比如Domino’sPizza最近推出了一个允许用户通过语音下单的移动APP。

      上面提到的认知技术进步飞快并吸引了大量投资,其他相对成熟的认知技术仍然是企业软件系统的重要组成部分。这些日渐成熟的认知技术包括决策最优化——自动完成对复杂决策或者在资源有限的前提下做出最佳权衡;规划和调度——使设计一系列行动流程来满足目标和观察约束;规则导向系统——为专家系统提供基础的技术,使用知识和规则的数据库来自动完成从信息中进行推论的处理过程。

      三、认知技术的广泛使用

      各种经济部门已经把认知技术运用到了多种商业职能中。

      1)银行业

      自动欺诈探测系统使用机器学习可以识别出预示着欺诈性付款行动的行为模式;借助语音识别技术能够自动完成电话客服;声音识别可以核实来电者的身份

      2)医疗健康领域

      美国有一半的医院采用自动语音识别来帮助医生自动完成医嘱抄录,而且使用率还在迅速增长;机器视觉系统自动完成乳房X光检查和其他医学影响的分析;IBM的Watson借助自然语言处理技术来阅读和理解大量医学文献,通过假设自动生成来完成自动诊断,借助机器学习可以提高准确率。

      3)生命科学领域

      机器学习系统被用来预测生物数据和化合物活动的因果关系,从而帮助制药公司识别出最有前景的药物。

      4)媒体与娱乐行业

      许多公司正在使用数据分析和自然语言生成技术,自动起草基于数据的的公文材料,比如公司营收状况、体育赛事综述等。

      5)石油与天然气

      厂商将机器学习广泛运用在矿藏资源定位、钻井设备故障诊断等众多方面。

      6)公共部门

      出于监控、合规和欺诈检测等特定目的,公共部门也已经开始使用认知技术。比如,乔治亚州正在通过众包的形式来进行财政披露和竞选捐助表格的数字化,在这个过程中他们就采用了一套自动手写识别系统。

      7)零售商

      零售商利用机器学习来自动发现有吸引力的交叉销售定价和有效的促销活动。

      8)科技公司

      它们正利用机器视觉、机器学习等认知技术来改进产品或者开发全新产品,比如Roomba机器人吸尘器,Nest智能恒温器。

      上述例子表明,认识技术的潜在商业收益远大于自动化带来的成本节约,这主要体现在:

      更快的行动与决策(比如,自动欺诈检测,计划和调度)

      更好的结果(比如,医学诊断、石油探测、需求预测)

      更高的效率(亦即,更好的利用高技能人才和昂贵设备)

      更低的成本(比如,自动电话客服减少了劳动成本)

      更大的规模(亦即,开展人力无法执行的大规模任务)

      产品与服务创新(从增加新功能到创造新产品)

      四、认知技术影响力与日俱增的原因

      在未来五年,认知技术在商业领域的影响力将显著增长。原因有二,首先,近些年来,技术性能有了实质进步,并处于持续研发状态。其次,数亿美元已经投入到技术商业化中,许多公司正致力于为各商业部门的广泛需求提供定制化开发和打包方案,以使这些技术更易购买和配置。虽然并非所有的技术提供商都能幸存,但他们的努力将共同推动市场前进。技术性能的改善和商业化正在共同扩大着认知技术的应用范围,这种情况在未来几年都将持续下去。

      1、技术提升扩展了应用范围

      认知技术大踏步前进的例子非常多。比如Google的语音识别系统,一份报告显示,Google用了不到两年时间就将语音识别的精准度从2012年的84%提升到如今的98%。计算机视觉技术也取得了突飞猛进的发展。如果以计算机视觉技术研究者设置的技术标准来看,自2010年到2014年,图像分类识别的精准度提高了4倍。Facebook的DeepFace技术在同行评审报告(译者注:同行评审,是一种学术成果审查程序,即一位作者的学术著作或计划被同一领域的其他专家学者评审。)被高度肯定,其脸部识别率的准确度达到97%。2011年,IBM为了让Watson在智力节目《危险边缘》中获胜,曾对Watson进行优化,提升两倍的答案精确度。现在,IBM又宣称如今的Watson比当时“智能”了2400%。

      随着技术的改进和提高,技术应用的范围也在不断扩大。比如,在语音识别方面,机器曾经需要大量训练才能在有限词库里勉强识别出来,由语音识别技术延伸出的医疗应用程序也很难得到真正普及。而现在,每个月互联网上都会有数以百万次的语音搜索。另外,计算机视觉技术过去被狭隘的理解为部署在工业自动化方面,但现在,我们早已看到这一技术被广泛运用到监控、安全以及各种各样的消费应用里。IBM如今正拓展Watson在竞赛游戏之外的应用,从医疗诊断到医学研究再到财务建议以及自动化的呼叫中心。

      并不是所有的认知技术都有如此令人瞩目的发展。机器翻译有了一定发展,但幅度很小。一份调查发现,从2009年到2012年,将阿拉伯语翻译到英语的精确度仅仅提升了13%。尽管这些技术还不完美,但他们已经可以影响到专业机构的工作方式。很多专业翻译人员依靠机器翻译提升翻译精准度,并把一些常规翻译交给机器,自己专注在更具挑战性的任务上。

      很多公司正努力将认知技术做进一步研发,并逐步将其融入到更多产品尤其是企业级产品里,以方便企业用户购买和部署。

      2、对商业化进行的大规模投资

      从2011年到2014年5月,超过20亿美元的风险投资流入到基于认知技术研究的产品和服务里。与此同时,超过100家的相关公司被兼并或收购,其中一些被互联网巨头如亚马逊、苹果、Google、IBM或Facebook收购。所有这些投资都在培育一个多样化的公司图谱,这些公司正在加速认知技术的商业化进程。

      在这里,我们并不会提供关于某公司在认知技术商业化方面的细节,我们希望说明,认知技术产品拥有丰富的多样性。下面就是致力于认知技术商业化的公司名单,这个名单既不是完整无缺也非固定不变,而是一个动态的,用于推动和培育市场的指标。

      数据管理和分析工具主要使用自然语言处理、机器学习等认知技术。这些工具利用自然语言处理来从非结构化的文本中提取出意思,或者借助机器学习帮助分析人员从大规模数据集中发现深层含义。这个领域的公司包括ContextRelevant(译者注:美国的一家大数据挖掘和分析公司)、PalantirTechnologies(译者注:这家公司称要将数据、技术、人类和环境连接起来)、以及Skytree(译者注:一家借助机器学习进行市场分析并提供决策依据的大数据公司)。

      认知技术的各个部分可以被整合到各种应用和商业决策中,分别起到增加功能和提高效率的作用。例如,Wise.io公司提供一套模块来促进商业决策,比如客户支持、营销和销售,这里面会用到机器学习模型来预测哪些客户比较容易流失,以及哪些潜在客户更加容易转化。Nuance公司通过提供一种语音识别技术来帮助开发者进行需要语音控制的移动APP的开发。

      单点解决方案。众多认知技术成熟的标志是它们正在被不断的嵌入到特定商业问题的解决方案中。这些解决方案的设计初衷是要比公司原有的解决方案更加有效,并且几乎不需要认知技术方面的专业人员。普及度比较高的应用领域包括广告、营销和销售自动化、预测以及规划。

      技术平台。平台的目的是为建立高度定制化的商业解决方案提供基础。它们会提供一系列功能,包括数据管理、机器学习工具、自然语言处理、知识表示和推理、以及将这些定制化软件整合在一起的统一框架。

      3、新兴应用

      如果这些技术的表现和商业化趋势继续发展,我们就能够大胆预测认知技术的应用将更加广泛,被接受程度也会大大增加。数亿美金的投资涌入这些基于机器学习、自然语言处理、机器视觉或者机器人技术的公司,这预示着许多新应用即将投入市场。在商业机构依托认知技术构建自动化业务流程、增强产品和服务方面,我们也看到了巨大空间。

      五、认知技术在企业的应用路径

      认知技术将在接下来几年里变得流行。在未来2-5年,技术层面的进步和商业化将扩大认知技术对企业的影响。越来越多的企业会找到一些创新性应用来显著改善他们自身的表现或者创造新功能,以增强他们的竞争地位。企业的IT部门现在可以行动起来,增加对这些技术的了解,评估出适用这些技术的机会,将这些技术可能带来的价值向领导进行汇报。高级商务和公共部门的领导应该思考认知技术将对他们的部门以及整个公司产生何种影响,这些技术将如何激发创新并提升经营表现。


    1.人工智能大发展时代,如何利用大数据改变现有商业模式?

    http://www.duozhishidai.com/article-17554-1.html

    2.人工智能来势凶猛,人工智能最热门的技术趋势是什么

    http://www.duozhishidai.com/article-923-1.html

    3.今年人工智能领域,十项中外人工智能领域富有突破性的技术

    http://www.duozhishidai.com/article-17560-1.html

     

    展开全文
  • WPF开发教程

    万次阅读 多人点赞 2019-07-02 23:13:20
    ------WPF 开发教程   目录 ...WPF基础入门.......目标就是提供在堆栈中处于较低位置的可扩展性点,这将允许外部开发人员可以在需要时创建自己的框架。 System.Windows.FrameworkElement ...

    ------WPF开发教程

     

    目录

    WPF基础入门.... 3

    1.    WPF基础之体系结构... 3

    2.    WPF基础之XAML. 9

    3.    WPF基础之基元素... 23

    4.    WPF基础之属性系统... 26

    5.    WPF基础之路由事件... 33

    6.    WPF基础之布局系统... 46

    7.    WPF基础之样式设置和模板化... 51

    8.    详谈WPF开发中的数据虚拟化... 64

    XAML语法.... 74

    1.    XAML语法术语... 74

    2.    代码隐藏和XAML. 82

    3.    XAML和自定义类... 83

    4.    标记扩展和XAML. 87

    5.    XAML命名空间和命名空间映射... 90

    6.    WPF名称范围... 92

    WPF控件开发.... 95

    1.    WPF控件开发之控件概述... 95

    2.    使用XAML创建按钮... 103

    3.    WPF控件库之Button. 114

    4.    WPF控件库之Menu. 115

    5.    WPF控件库之Lable. 119

    6.    WPF控件库之Toolbar. 121

    7.    WPF控件开发之自定义控件... 124

    8.    WPF控件开发之装饰器... 140

    WPF数据绑定.... 143

    1.    数据绑定概述... 143

    2.    WPF数据绑定之绑定源... 164

    3.    WPF数据绑定之数据模板... 166

    4.    WPF数据绑定之绑定声明... 181

    5.    实例一:绑定到ADO.NET数据源... 184

    6.    实例二:绑定到LINQ查询的结果... 186

    WPF图形和多媒体开发.... 187

    1.    WPF 图形动画和媒体概述... 187

    2.    WPF的图形呈现... 191

    3.    WPF的图像处理... 205

    4.    WPF的三维图形应用... 219

    5.    WPF的三维变换应用... 229

    6.    WPF的动画开发... 238

    7.    WPF的多媒体开发... 250

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    WPF基础入门

    1. WPF基础之体系结构

    本主题提供 Windows Presentation Foundation (WPF) 类层次结构的指导教程,涵盖了 WPF 的大部分主要子系统,并描述它们是如何交互的。本主题还详细介绍了 WPF 架构师所做的一些选择。

    System.Object

    WPF 主要编程模型是通过托管代码公开的。在 WPF 的早期设计阶段,曾有过大量关于如何界定系统的托管组件和非托管组件的争论。CLR 提供一系列的功能,可以令开发效率更高并且更加可靠(包括内存管理、错误处理和通用类型系统等),但这是需要付出代价的。

    下图说明了 WPF 的主要组件。关系图的红色部分(PresentationFramework、PresentationCore 和 milcore)是 WPF 的主要代码部分。在这些组件中,只有一个是非托管组件 – milcore。milcore 是以非托管代码编写的,目的是实现与 DirectX 的紧密集成。WPF 中的所有显示是通过 DirectX 引擎完成的,可实现高效的硬件和软件呈现。WPF 还要求对内存和执行进行精确控制。milcore 中的组合引擎受性能影响关系大,需要放弃 CLR 的许多优点来提高性能。

    本主题的后面部分将讨论 WPF 的托管和非托管部分之间的通信。下面介绍托管编程模型的其余部分。

    System.Threading.DispatcherObject

    WPF 中的大多数对象是从 DispatcherObject 派生的,这提供了用于处理并发和线程的基本构造。WPF 基于调度程序实现的消息系统。其工作方式与常见的 Win32 消息泵非常类似;事实上,WPF 调度程序使用 User32 消息执行跨线程调用。

    要讨论 WPF 中的并发,首先必须真正理解两个核心概念 – 调度程序和线程关联。

    在 WPF 的设计阶段,目标趋向于单一线程的执行,但这不是一种与线程“关联的”模型。当一个组件使用执行线程的标识来存储某种类型的状态时,将发生线程关联。最常见的形式是使用线程本地存储 (TLS) 来存储状态。线程关联要求执行的每个逻辑线程仅由操作系统中的一个物理线程所拥有,这将占用大量内存。最后,WPF 的线程处理模型保持与具有线程关联的单一线程执行的现有 User32 线程处理模型同步。主要原因是互操作性 – 类似于 OLE 2.0 的系统、剪贴板和 Internet Explorer 均需要单一线程关联 (STA) 执行。

    假设您具有带有 STA 线程的对象,则需要一种方式来在线程之间通信,并验证您是否位于正确的线程上。调度程序的作用就在于此。调度程序是一个基本的消息调度系统,具有多个按优先级排列的队列。消息的示例包括原始输入通知(鼠标移动)、框架函数(布局)或用户命令(执行此方法)。通过从 DispatcherObject 派生,您可以创建一个具有 STA 行为的 CLR 对象,并在创建时获得一个指向调度程序的指针。

    System.Windows.DependencyObject
    生成 WPF 时使用的主要体系结构原理之一是首选属性而不是方法或事件。属性是声明性的,使您更方便地指定意图而不是操作。它还支持模型驱动或数据驱动的系统,以显示用户界面内容。这种理念的预期效果是创建您可以绑定到的更多属性,从而更好地控制应用程序的行为。

    为了从由属性驱动的系统获得更多,需要一个比 CLR 提供的内容更丰富的属性系统。此丰富性的一个简单示例就是更改通知。为了实现双向绑定,您需要绑定的双方支持更改通知。为了使行为与属性值相关联,您需要在属性值更改时得到通知。Microsoft .NET Framework 具有一个 INotifyPropertyChange 接口,使对象可以发布更改通知(不过,这是可选的)。

    WPF 提供一个丰富的属性系统,该属性系统是从 DependencyObject 类型派生的。该属性系统实际是一个“依赖”属性系统,因为它会跟踪属性表达式之间的依赖关系,并在依赖关系更改时自动重新验证属性值。例如,如果您具有一个会继承的属性(如 FontSize),当继承该值的元素的父级发生属性更改时,会自动更新系统。

    WPF 属性系统的基础是属性表达式的概念。在 WPF 的第一版中,属性表达式系统是关闭的,表达式都是作为框架的一部分提供的。表达式致使属性系统不具有硬编码的数据绑定、样式调整或继承,而是由框架内后面的层来提供这些功能。

    属性系统还提供属性值的稀疏存储。因为对象可以有数十个(如果达不到上百个)属性,并且大部分值处于其默认状态(被继承、由样式设置等),所以并非对象的每个实例都需要具有在该对象上定义的每个属性的完全权重。

    属性系统的最后一个新功能是附加属性的概念。WPF 元素是基于组合和组件重用的原则生成的。某些包含元素(如 Grid 布局元素)通常需要子元素上的其他数据才能控制其行为(如行/列信息)。任何对象都可以为任何其他对象提供属性定义,而不是要将所有这些属性与每个元素相关联。这与 JavaScript 中的“expando”功能相似。

    System.Windows.Media.Visual

    定义一个系统后,下一步是将像素绘制到屏幕上。Visual 类用于生成可视化对象的树,每个对象可以选择性地包含绘制指令以及有关如何呈现这些指令(剪辑、变换等)的元数据。Visual 设计为极其轻量且灵活,所以大部分功能未进行 API 公开,并且极为依赖受保护的回调函数。

    Visual 实际上是到 WPF 组合系统的入口点。Visual 是以下两个子系统之间的连接点:托管 API 和非托管 milcore。

    WPF 通过遍历由 milcore 管理的非托管数据结构来显示数据。这些结构(称为组合节点)代表层次结构显示树,其中每个节点都有呈现指令。只能通过消息传递协议来访问此树(下图右侧所示)。

    当对 WPF 编程时,您将创建 Visual 元素及派生的类型,它们通过此消息传递协议在内部与此组合树进行通信。WPF 中的每个 Visual 可以不创建组合节点,也可以创建一个或多个组合节点。

    请注意一个非常重要的体系结构细节 – 可视对象和绘制指令的整个树都要进行缓存。在图形方面,WPF 使用一个保留的呈现系统。这可以使系统以一个高刷新率重绘系统,并且不会发生组合系统阻止对用户代码的回调。这有助于防止出现应用程序无响应的情况。

    关系图中不十分引人注意的另一个重要细节是系统实际上如何执行组合。

    在 User32 和 GDI 中,系统是在一个即时模式剪辑系统上工作。当需要呈现一个组件时,系统会建立一个剪辑边界,不允许组件接触该边界之外的像素,然后会要求此组件在该框中绘制像素。此系统在内存受限的系统上工作良好,因为当某些内容更改时,只需要处理受影响的组件即可 – 不会有两个组件对一个像素的颜色更改起作用。

    WPF 使用“绘画器的算法”绘制模型。这意味着并不是剪辑每个组件,而是要求从显示内容的背面至正面来呈现每个组件。这允许每个组件在先前的组件的显示内容上绘制。此模型的优点是您可以生成部分透明的复杂形状。与现今的现代图形硬件比较,此模型相对要快(创建 User32/ GDI 的情况除外)。

    如上面所述,WPF 的一个核心原理是移动到一个更具声明性且“以属性为核心”的编程模型。在可视化系统中,这会表现为需要关注的两种情况。

    首先,如果您考虑保留的模式图形系统,则实际上是从命令性 DrawLine/DrawLine 类型模型移动到面向数据的模型 new Line()/new Line()。通过这一向数据驱动的呈现移动,可以在使用属性表达的绘制指令上进行复杂的操作。从 Drawing 派生的类型实际上是用于呈现的对象模型。

    第二,如果评估动画系统,您将看到它几乎是完全声明性的。无需要求开发人员计算下一位置或下一颜色,您可以将动画表示为动画对象上的一组属性。于是,这些动画可以表示开发人员或设计人员的意图(在 5 秒内将此按钮从一个位置移动到另一个位置),系统就可以确定完成此任务的最有效方式。

    System.Windows.UIElement

    UIElement 定义核心子系统,包括 Layout、Input 和 Event。

    Layout 是 WPF 中的一个核心概念。在许多系统中,可能有一组固定的布局模型(HTML 支持三种布局模型:流、绝对和表),也可能没有布局模型(User32 实际仅支持绝对定位)。WPF 先假设开发人员和设计人员希望有一个灵活的可扩展布局模型,该模型可能是由属性值而不是命令性逻辑驱动的。在 UIElement 级别,会引入布局的基本协定 - 具有 Measure 和 Arrange 处理过程的两阶段模型。

    Measure 允许组件确定它要采用的大小。此阶段独立于 Arrange,因为在许多情形下,父元素会要求子元素测量若干次以确定其最佳位置和大小。父元素要求子元素测量这一事实体现了 WPF 的另一关键原则 – 内容大小。WPF 中的所有控件支持调整到内容原始大小的功能。这使本地化更加容易,并允许在调整大小时对元素进行动态布局。Arrange 阶段允许父元素定位并确定每个子元素的最终大小。

    通常会花费大量的时间来讨论 WPF 的输出端(Visual 及其相关对象)。然而,在输入端也有许多创新。WPF 输入模型的最基本更改也许是一致模型,输入事件通过系统借助此模型进行路由。

    输入是作为内核模式设备驱动程序上的信号发出的,并通过涉及 Windows 内核和 User32 的复杂进程路由到正确的进程和线程。与输入相对应的 User32 消息一旦路由到 WPF,它就会转换为 WPF 原始输入消息,并发送到调度程序。WPF 允许原始输入事件转换为多个实际事件,允许在保证传递到位的情况下在较低的系统级别实现类似“MouseEnter”的功能。

    每个输入事件至少会转换为两个事件 – “预览”事件和实际事件。WPF 中的所有事件都具有通过元素树路由的概念。如果事件从目标向上遍历树直到根,则被称为“冒泡”,如果从根开始向下遍历到目标,它们被称为“隧道”。输入预览事件隧道,使树中的任何元素都有机会筛选事件或对事件采取操作。然后,常规(非预览)事件将从目标向上冒泡到根。

    分割隧道和冒泡阶段使快捷键等功能在复合世界中表现一致。在 User32 中,您可以通过使用一个全局表来实现快捷键,该表中包含您希望支持的所有快捷键(Ctrl+N 映射为“新建”)。在应用程序的调度程序中,您可以调用 TranslateAccelerator,它会探查 User32 中的输入消息,并确定是否有任何消息与已注册的快捷键匹配。在 WPF 中,上述内容不会起作用,因为系统是完全“可组合”的 – 任何元素都可以处理和使用任何快捷键。将这个两阶段模型用于输入,将允许组件实现其自己的TranslateAccelerator"。

    为了进一步深化此功能,UIElement 还引入了 CommandBindings 的概念。WPF 命令系统允许开发人员以命令终结点(一种用于实现 ICommand 的功能)的方式定义功能。命令绑定使元素可以定义输入笔势 (Ctrl+N) 和命令(“新建”)之间的映射。输入笔势和命令定义都是可扩展的,并且可以在使用时联系到一起。这使得一些操作(例如,允许最终用户自定义其要在应用程序内使用的键绑定)显得无关紧要。

    至此,本主题已重点讨论了 WPF 的“核心”功能 - PresentationCore 程序集中实现的功能。当生成 WPF 时,基础部分(例如带有 Measure 和 Arrange 的布局的协定)和框架部分(例如 Grid 的特定布局的实现)之间的明确划分是希望的结果。目标就是提供在堆栈中处于较低位置的可扩展性点,这将允许外部开发人员可以在需要时创建自己的框架。

    System.Windows.FrameworkElement

    可以按两种不同的方式来看待 FrameworkElement。它对在 WPF 的较低层中的子系统引入一组策略和自定义项。它还引入了一组新的子系统。

    FrameworkElement 引入的主要策略是关于应用程序布局。FrameworkElement 在 UIElement 引入的基本布局协定之上生成,并增加了布局“插槽”的概念,使布局制作者可以方便地拥有一组面向属性的一致的布局语义。HorizontalAlignment、VerticalAlignment、MinWidth 和 Margin 等属性使得从 FrameworkElement 派生的所有组件在布局容器内具有一致的行为。

    利用 FrameworkElement,WPF 的核心层中具有的许多功能可以更方便地进行 API 公开。例如,FrameworkElement 通过 BeginStoryboard 方法提供对动画的直接访问。Storyboard 提供一种针对一组属性为多个动画编写脚本的方式。

    FrameworkElement 引入的两个最关键的内容是数据绑定和样式。

    曾经使用 Windows 窗体或 ASP.NET 创建应用程序用户界面 (UI) 的用户应当对 WPF 中的数据绑定子系统较为熟悉。在上述每个系统中,可通过一种简单的方式来表达您希望将给定元素中的一个或多个属性绑定到一个数据片断。WPF 对属性绑定、变换和列表绑定提供全面支持。

    WPF 中数据绑定的最值得关注的功能之一是引入了数据模板。利用数据模板,您可以声明性地指定某个数据片断的可视化方式。您可以将问题换个方向,让数据来确定将要创建的显示内容,而无需创建可绑定到数据的自定义用户界面。

    样式实际上是轻量级的数据绑定。使用样式,您可以将共享定义的一组属性绑定到元素的一个或多个实例。通过显式引用(通过设置 Style 属性)或通过将样式与元素的 CLR 类型隐式关联,便可以将样式应用到元素。

    System.Windows.Controls.Control

    控件的最重要的功能是模板化。如果您将 WPF 的组合系统视为一个保留模式呈现系统,则控件可通过模板化以一种参数化的声明性方式描述其呈现。ControlTemplate 实际上不过是一个用于创建一组子元素的脚本,同时绑定到由控件提供的属性。

    Control 提供一组常用属性,如 Foreground、Background、Padding 等,模板创作者可以使用这些常用属性来自定义控件的显示。控件的实现提供了数据模型和交互模型。交互模型定义了一组命令(如窗口的“关闭”),以及到输入笔势的绑定(如单击窗口上角的红叉)。数据模型提供了一组属性,用于自定义交互模型或自定义显示(由模板确定)。

    数据模型(属性)、交互模型(命令和事件)及显示模型(模板)之间的划分,使用户可以对控件的外观和行为进行完全自定义。

    最常见的控件数据模型是内容模型。如果查看 Button 之类的控件,您会看到它具有一个类型为 Object 的名为“Content”的属性。在 Windows 窗体和 ASP.NET 中,此属性通常为一个字符串 – 不过,这会限制您可以在按钮中输入的内容类型。按钮的内容可以是简单的字符串、复杂的数据对象或整个元素树。如果是数据对象,可以使用数据模板构造显示内容。

    摘要

    WPF 旨在帮助您创建动态的数据驱动的演示系统。系统的每一部分均可通过驱动行为的属性集来创建对象。数据绑定是系统的基础部分,在每一层中均进行了集成。

    传统的应用程序创建一个显示内容,然后绑定到某些数据。在 WPF 中,关于控件的所有内容、显示内容的所有方面都是由某种类型的数据绑定生成的。通过在按钮内部创建复合控件并将其显示绑定到按钮的内容属性,就会显示按钮内的文本。

    当开始开发基于 WPF 的应用程序时,您应对其感到非常熟悉。在其中设置属性、使用对象和数据绑定的方式与您使用 Windows 窗体或 ASP.NET 是极其相似的。如果对 WPF 体系结构有更深的了解,您就能够创建更丰富的应用程序,这些应用程序在根本上会将数据视为应用程序的核心驱动力。

    1. WPF基础之XAML

    本主题介绍可扩展应用程序标记语言 (XAML) 语言的功能,并演示如何使用 XAML 编写 Windows Presentation Foundation (WPF) 应用程序。本主题专门介绍了 Windows Presentation Foundation (WPF) 实现的 XAML。XAML 本身是比 Windows Presentation Foundation (WPF) 更广泛的一个语言概念。

    具有流控制支持的声明性语言
    XAML 简化了为 .NET Framework 编程模型创建 UI 的过程。您可以在声明性 XAML 标记中创建可见的 UI 元素,然后使用代码隐藏文件(通过分部类定义与标记相连接)将 UI 定义与运行时逻辑相分离。在 XAML 中混合代码和标记的功能很重要,因为 XML 本身是声明性的,不会为流控制真正建议一个模型。基于 XML 的声明性语言非常直观,可以为用户(尤其是具有 Web 设计和技术背景的人员)创建从原型到生产的各种界面。与其他大多数标记语言不同,XAML 直接呈现托管对象的实例化。这种常规设计原则简化了使用 XAML 创建的对象的代码和调试访问。

    XAML 文件是指通常使用 .xaml 扩展名的 XML 文件。

    下面的 XAML 示例演示了小标记在创建作为 UI 一部分的按钮时的必要性。创建的按钮通过主题样式获得默认的可视化表示形式,通过其类设计获得默认的行为。

    XAML 对象元素
    XAML 有一组规则,这些规则将对象元素映射为类或结构,将属性 (Attribute) 映射为属性 (Property) 或事件,并将 XML 命名空间映射为 CLR 命名空间。XAML 元素映射为被引用程序集中定义的 Microsoft .NET 类型,而属性 (Attribute) 则映射为这些类型的成员。

    上面的示例指定了两个对象元素:<STACKPANEL>(具有一个结束标记)和<BUTTON>同样具有多个属性;下一节将介绍属性)。字符串 StackPanel 和 Button 都将映射为某个类的名称,该类由 WPF 定义并且是 WPF 程序集的一部分。在指定对象元素标记时,可以为 XAML 处理创建一条指令,以便在加载 XAML 页时创建指定类的一个新实例。每个实例都是通过调用基础类或结构的默认构造函数并对结果进行存储而创建的。为了可用作 XAML 中的对象元素,该类或结构必须公开一个公共的默认(无参数)构造函数。

    设置属性
    XAML 中的属性是通过使用各种可能的语法在对象元素上设置属性来设置的。根据所设置的属性的特征,给定属性可使用的语法会有所不同。

    通过设置属性值,可以为对象元素添加功能或特征。对象元素的基础对象实例的初始状态基于默认的构造函数行为。通常,您的应用程序将使用其他一些实例,而不是任何给定对象的完全默认的实例。

    属性语法
    在 XAML 中,属性 (Property) 常常可以表示为属性 (Attribute)。属性 (Attribute) 语法是最简单的属性 (Property) 设置语法,并将成为过去使用标记语言的开发人员可以使用的最直观的语法。例如,以下标记将创建一个具有红色文本和蓝色背景的按钮,还会创建指定为 Content 的显示文本。

    属性元素语法
    对于一个对象元素的某些属性 (Property),属性 (Attribute) 语法是不可能实现的,因为提供属性 (Property) 值所需的对象或信息不能充分地表示为简单的字符串。对于这些情况,可以使用另一个语法,即属性元素语法。属性元素语法用标记的内容设置包含元素的引用的属性。一般而言,内容就是作为属性值的类型的某个对象(值设置实例通常被指定为另一个对象元素)。属性元素本身的语法为 <类型名称.属性>。指定内容之后,必须用一个结束标记结束属性元素,就像其他任何元素(语法为 )一样。对于同时支持属性 (Attribute) 和属性 (Property) 元素语法的属性 (Property),尽管这两种语法的细微之处(如空白处理)略有不同,但它们的结果通常是一样的。如果可以使用属性 (Attribute) 语法,那么使用属性 (Attribute) 语法通常更为方便,且能够实现更为精简的标记,但这只是一个风格的问题,而不属于技术限制。下面的示例演示了在前面的属性 (Attribute) 语法示例中设置的相同属性 (Property),但这次对 Button 的所有属性 (Property) 使用了属性 (Property) 元素语法。

    XAML 的属性 (Property) 元素语法表示了与标记的基本 XML 解释之间的巨大背离。对于 XML,<类型名称.属性> 代表了另一个元素,该元素仅表示一个子元素,而与 TypeName 父级之间没有必然的隐含关系。在 XAML 中,<类型名称.Property> 直接表示 Property 是类型名称 的属性(由属性元素内容设置),而绝不会是一个名称相似(碰巧名称中有一个点)但却截然不同的元素。

    属性和类继承
    作为 WPF 元素的XAML 属性 (Attribute) 而出现的属性 (Property) 通常从基类继承而来。例如,在上一个示例中,如果您要查看类定义、反射结果或文档,Background 属性并不是在 Button 类上直接声明的属性。相反,Background 是从基 Control 类继承而来。

    WPF XAML 元素的类继承行为是与标记的基本 XML 解释之间的另一个巨大背离。使用类继承(尤其是中间基类为抽象类时)的另一个原因在于,通过 XML 编程常用的架构类型(如 DTD 或 XSD 格式)几乎不可能准确且完整地表示 XAML 元素及其允许属性集。另外,XAML 中的“X”表示“extensible”(可扩展),而可扩展性破坏了“什么是用于 WPF 的 XAML”的任何给定表示形式的完整性。

    引用值和标记扩展
    标记扩展是一个 XAML 概念。在属性语法中,花括号({ 和 })表示标记扩展用法。此用法指示 XAML 处理不要像通常那样将属性值视为一个字符串或者可直接转换为文本字符串的值。

    当属性采用引用类型值时,这些属性常常需要属性元素语法(始终创建一个新实例)或通过标记扩展的对象引用。标记扩展用法有可能会返回现有实例,因此可以更加多样化,或者产生较少的对象系统开销。

    当使用标记扩展提供属性值时,应改为由相关标记扩展的后备类中的逻辑提供属性值。WPF 应用程序编程中最常用的标记扩展是 Binding(用于数据绑定表达式)以及资源引用 StaticResource 和 DynamicResource。通过使用标记扩展,即使属性 (Property) 不支持对直接对象实例化使用属性 (Attribute) 语法,也可以使用属性 (Attribute) 语法为属性 (Property) 提供引用值;或者使特定行为能够符合必须用属性 (Property) 类型值填充 XAML 属性 (Property) 这一常规行为要求。

    例如,下面的示例使用属性 (Attribute) 语法设置 Style 属性 (Property) 的值。Style 属性 (Property) 采用了 Style 类的一个实例,这是默认情况下不能在属性 (Attribute) 语法字符串中指定的引用类型。但在本例中,属性 (Attribute) 引用了特定的标记扩展 StaticResource。当处理该标记扩展时,它返回对以前在资源字典中作为键控资源进行实例化的某个样式的引用。

    资源只是 WPF 或 XAML 启用的一种标记扩展用法。

    支持 Typeconverter 的属性值
    在“属性语法”一节中,曾提到属性值必须能够使用字符串进行设置。对字符串如何转换为其他对象类型或基元值的基本本机处理取决于 String 类型本身。但是很多 WPF 类型或这些类型的成员扩展了基本字符串属性处理行为,因此更复杂的对象类型的实例可通过字符串指定为属性值。在代码级别,此处理是通过指定处理字符串属性值的 CLR 类型转换器来完成的。常用于指示矩形区域尺寸(如 Margin)的 Thickness 结构类型是这样一个类型的示例:它具有针对采用该类型的所有属性 (Property) 公开的一个特殊的、支持类型转换器的属性 (Attribute) 语法,以便于在 XAML 标记中使用。下面的示例使用支持类型转换器的属性 (Attribute) 语法来为 Margin 提供值:

    上面的属性 (Attribute) 语法示例与下面更为详细的语法示例等效,但在下面的示例中,Margin 是通过包含 Thickness 对象元素的属性 (Property) 元素语法设置的,而且 Thickness 的四个关键属性 (Property) 设置为新实例的属性 (Attribute):

    是使用支持类型转换器的语法,还是使用更详细的等效语法,通常只是编码风格的选择问题,但支持转换器的语法有助于生成更简洁的标记。(但是,有一些对象只能采用类型转换器将属性设置为该类型,因为类型对象本身并没有默认的构造函数。例如,Cursor。)

    集合类型和 XAML 集合属性
    XAML 指定了一个语言功能,通过该功能,可以从标记中特意省略表示集合类型的对象元素。当 XAML 处理器处理采用了集合类型的属性时,将隐式创建相应集合类型的实例,即使标记中不存在该集合的对象元素也是如此。在集合类型的 SDK 参考页中,特意省略集合对象元素的这种语法在 XAML 语法部分中有时候称为“隐式集合语法”。

    隐式集合语法适用于实现 IList 或 IDictionary 的类型,或者适用于数组。

    您已经在 XAML 资源示例中看到了未调用的集合对象元素的隐式集合语法的示例:

    除了根元素外,页面上作为另一个元素的子元素而嵌套的每个对象元素实际上都是下列一种或两种情况下的元素:父元素的隐式集合属性的一个成员,或者为父元素指定 XAML 内容属性值的元素(XAML 内容属性将在下一节进行讨论)。换言之,一个标记页上的父元素与子元素之间的关系实际上就是一个根对象,而根对象下面的每个对象元素要么是为父元素提供属性值的一个实例,要么是同样作为父元素的集合类型属性值的集合中的一项。在资源示例的案例中,Resources 属性采用 ResourceDictionary 类型的一个对象。下面的示例在语法上与显式指定的 ResourceDictionary 的对象元素等效。

    Resources 集合是许多常见的 WPF 框架级元素上存在的集合属性的一个示例。在 XAML 中设置此属性需要使用属性元素语法。属性元素中的每个被包含的对象元素都成为集合(IDictionary 实现)中的一个项。虽然集合类型本身通常没有包含项的属性或索引器,但是该属性不能在标记中指定;它完全是隐含的。对于 ResourceDictionary,该属性是 Item 索引器。

    XAML 内容属性
    XAML 指定了一个语言功能,通过该功能,任何可以用作 XAML 对象元素的类都可以确切指定其属性之一作为该类实例的 XAML 内容属性。当 XAML 处理器处理具有 XAML 内容属性的对象元素时,该对象元素的任何 XML 子元素都被当作包含在一个表示该内容属性的隐式属性元素标记中来处理。在标记中,可以省略 XAML 内容属性的属性元素语法。在标记中指定的任何子元素都将成为 XAML 内容属性的值。

    您已经看过了未调用的 XAML 内容属性的示例:本主题中的第一个示例。

    这里,Button 是 StackPanel 的子元素。这是一个简单直观的标记,其中出于两个不同的原因省略了两个标记。

    省略的 StackPanel.Children 属性元素: StackPanel 从 Panel 派生。Panel 将 Panel..::.Children 定义为其 XAML 内容属性。Panel 的所有派生类因而具有该 XAML 内容属性,而 Panel..::.Children 的属性元素可省略。

    省略的 UIElementCollection 对象元素: Panel..::.Children 属性采用类型 UIElementCollection,该类型实现 IList。因此,根据为集合定义的 XAML 规则,可以省略 UIElementCollection 对象元素标记。在这种情况下,UIElementCollection 实际上不能实例化为一个对象元素。您甚至无法显式声明该集合对象。这是因为 UIElementCollection 不公开默认的构造函数。其他几个 WPF 集合类型也不公开对象元素用法的构造函数,因为 XAML 集合语法处理仍然允许它们在 XAML 中隐式工作。这就是 UIElementCollection 对象元素在示例中显示为已被注释的原因;如果未被注释,示例将不能编译。

    内部文本和 XAML 内容属性
    StackPanel / Button 示例还有另一种变体。

    请注意为 Button 指定的显示文本如何发生变化。前面已在属性 (Attribute) 语法中指定了 Content 属性 (Property);这次显示字符串是 Button 对象元素中的内部文本。此语法可行,因为 Content 是 Button 基类 ContentControl 的 XAML 内容属性。元素中的字符串根据 Content 属性的属性类型(即 Object)进行计算。Object 不会尝试任何字符串类型转换,因此 Content 属性的值变成了文本字符串值。或者,Button 中的内容可以是任何单个 Object。Button 等控件通常为类定义 XAML 内容属性,因此 XAML 内容属性可用于 UI 和显示文本,或用于控件合成,或同时用于此两者。

    对于流程文档模型和本地化而言,在元素中放置字符串作为内容以生成与其他常见标记语言类似的标记的功能特别重要。

    XAML 内容属性值必须连续
    XAML 内容属性的值必须完全在该对象元素的其他任何属性元素之前或之后指定。不管 XAML 内容属性的值指定为字符串还是指定为一个或多个对象都是如此。例如,下面的标记无法进行编译:

    这在本质上是非法的,因为如果此语法是通过使用内容属性的属性元素语法而变为显式的,则内容属性将设置两次:

    一个类似的非法示例是,如果内容属性是一个集合,则子元素是与属性元素交错的:

    内容模型
    从语法上讲,可能支持将类用作 XAML 元素,但只有放置到整体内容模型或元素树中的所需位置时,该元素才能在应用程序或页面上正常运行。例如,MenuItem 通常只应作为 MenuBase 派生类(如 Menu)的子级放置。特定元素的内容模型在可用作 XAML 元素的控件和其他 WPF 类的类页面上的备注中进行说明。对于具有更复杂内容模型的某些控件,内容模型作为单独的概念主题进行说明。

    XAML 中的大小写和空白
    XAML 区分大小写。按名称与程序集中的基础类型进行比较或者与类型的成员进行比较时,必须使用正确的大小写指定所有对象元素、属性 (Property) 元素和属性 (Attribute) 名称。属性的值并不总是区分大小写。值是否区分大小写将取决于与采用该值的属性关联的类型转换器行为,或取决于属性值类型。例如,采用 Boolean 类型的属性可以采用 true 或 True 作为等效值,但只是因为 Boolean 的默认字符串类型转换已经允许这些值作为等效值。

    XAML 处理器和序列化程序将忽略或删除所有无意义的空白,并规范化任何有意义的空白。只有当您在 XAML 内容属性中指定字符串时,才会体现此行为的重要性。简言之,XAML 将空格、换行符和制表符转化为空格,如果它们出现在一个连续字符串的任一端,则保留一个空格。

    有关 XAML 语法的更多信息
    隐式集合语法和 XAML 内容属性都是允许省略某些推断标记的 XAML 语言功能。这些功能的目的是在编写或检查标记时使页面上的元素的父子关系更明显。

    如果您正在创建自定义类,并且正在考虑是否允许使用 XAML,XAML 语法术语主题也是一个很好的起点。

    XAML 根元素和 xmlns
    一个 XAML 文件只能有一个根元素,这样才能成为格式正确的 XML 文件和有效的 XAML 文件。通常,应选择属于应用程序模型一部分的元素(例如,为页面选择 Window 或 Page,为外部字典选择 ResourceDictionary,或为应用程序定义根选择 Application)。下面的示例演示 WPF 页面的典型 XAML 文件的根元素,其中的根元素为 Page。

    根元素还包含属性 xmlns 和 xmlns:x。这些属性向 XAML 处理器指明哪些命名空间包含标记将要引用的元素的元素定义。xmlns 属性专门指示默认的 xmlns 命名空间。在默认的 xmlns 命名空间中,可以不使用前缀指定标记中的对象元素。对于大多数 WPF 应用程序方案以及 SDK 的 WPF 部分中给出的几乎所有示例,默认的 xmlns 命名空间均映射为 WPF 命名空间 http://schemas.microsoft.com/winfx/2006/xaml/presentation。xmlns:x 属性指示另外一个 xmlns 命名空间,该命名空间映射 XAML 语言命名空间 http://schemas.microsoft.com/winfx/2006/xaml。在具有此映射的文件的标记中引用时,XAML 规范定义的必需语言组件带有 x: 前缀。使用 xmlns 定义用法范围和映射的这种做法符合 XML 1.0 规范。请注意,xmlns 属性仅在每页的根元素上和应用程序定义上(如果在标记中提供了应用程序定义)才是严格必需的。xmlns 定义将应用于根的所有子元素。(此行为仍然符合 xmlns 的 XML 1.0 规范。)xmlns 属性还允许出现在根下面的其他元素上,并且将应用于定义元素的任何子元素。但是,此用法并不典型,因为频繁定义或重新定义 xmlns 命名空间可能会导致 XAML 标记样式难以阅读。

    由于存在属于项目生成文件一部分的配置,因此可以知道 WPF 程序集包含的某些类型支持 WPF 到默认 xmlns 的映射。程序集还映射到目标文件中。因此,为了引用来自 WPF 程序集的 XAML 元素,只需映射 xmlns 即可。对于您自己的自定义程序集,或者除 WPF 之外的程序集,可以将该程序集指定为 xmlns 映射的一部分。通常,可选择其他前缀,但是也可以选择其他 xmlns 作为默认值,然后将 WPF 映射到前缀。

    x: 前缀
    在前面的根元素示例中,前缀 x: 用于映射 XAML xmlns http://schemas.microsoft.com/winfx/2006/xaml。在此 SDK 的项目模板、示例以及文档中,此 x: 前缀将用于映射 XAML xmlns。x: 前缀/XAML xmlns 包含多个将在 XAML 中频繁用到的编程构造。下面列出了将用到的最常见 x: 前缀/XAML xmlns 编程构造:

    x:Key:为 ResourceDictionary 中的每个资源设置一个唯一的键。在应用程序标记中看到的所有 x: 用法中,x:Key 可能占到 90%。

    x:Class:向为 XAML 页提供代码隐藏的类指定 CLR 命名空间和类名。必须具有这样一个类才能支持代码隐藏,也正是由于这个原因,即使没有资源,您也几乎总是会看到映射的 x:。

    x:Name:处理对象元素后,为运行时代码中存在的实例指定运行时对象名称。在不支持等效的 WPF 框架级Name 属性的情况下命名元素时,可以使用 x:Name。某些动画方案中会发生这种情况。

    x:Static:启用一个获取静态值的值引用,该静态值只能是一个 XAML 可设置属性。

    x:Type:根据类型名称构造一个 Type 引用。它用于指定采用 Type 的属性 (Attribute),如 Style..::.TargetType,不过在许多情况下属性 (Property) 本身具有字符串到 Type 的转换功能,因此使用 x:Type 是可选的。

    x: 前缀/XAML xmlns 中还有其他一些不太常见的编程构造。
    事件和 XAML 代码隐藏
    大多数 WPF 应用程序都是既包括标记,又包括代码隐藏。在一个项目中,XAML 被编写为 .xaml 文件,而使用 CLR 语言(如 Microsoft Visual Basic .NET 或 C#)编写代码隐藏文件。编译 XAML 文件时,每个 XAML 页的 XAML 代码隐藏文件的位置是通过指定一个命名空间和类作为 XAML 页的根元素的 x:Class 属性来确定的。

    在目前已介绍的示例中,您已看到几个按钮,但还没有一个按钮具有任何关联的逻辑行为。为对象元素添加行为的主要应用程序级机制是使用元素类的现有事件,并为在运行时引发该事件时调用的该事件编写特定的处理程序。事件名称以及要使用的处理程序的名称在标记中指定,而实现处理程序的代码在代码隐藏中定义。

    请注意,代码隐藏文件使用命名空间 MyNamespace 并将 MyPageCode 声明为该命名空间内的一个分部类。这相当于在标记根中提供的 MyNamespace.MyPageCode 的 x:Class 属性值。编译器将通过从根元素类型派生一个类,自动为编译的任何 XAML 页创建一个分部类。当您提供也会定义同一分部类的代码隐藏时,将在与编译的应用程序相同的命名空间和类中组合生成的代码。

    如果您不想创建单独的代码隐藏文件,还可以将代码内联到 XAML 文件中。但是,内联代码是一种缺少多样性的方法,有很多的限制。

    事件属性语法
    当您在标记中通过事件指定行为时,通常使用属性语法来附加处理程序。在其中指定事件属性的对象元素则变成侦听事件以及调用处理程序的实例。您要处理的具体事件的名称是属性名。属性值是您要定义的处理程序的方法名。然后您必须在代码隐藏中提供处理程序实现,并使处理程序基于该事件的委托。您使用编程语言(如 Microsoft Visual Basic .NET 或 C#)在代码隐藏中编写处理程序。

    引发事件时,每个 WPF 事件都将报告事件数据。事件处理程序可以访问这些事件数据。在前面的示例中,处理程序通过事件数据获取所报告的事件源,然后在该事件源上设置属性。

    路由事件
    路由事件是一个特殊的事件功能,该功能是 WPF 特有的并且是它的基础。路由事件允许一个元素处理另一个元素引发的事件,只要这些元素通过元素树关系连接起来。当使用 XAML 属性指定事件处理时,可以在任何元素(包括未在类成员表中列出该特定事件的元素)上侦听和处理路由事件。这是通过使用所属类名限定事件名属性来实现的。例如,在当前所讨论的 StackPanel / Button 示例中,父 StackPanel 可以通过在 StackPanel 对象元素上指定属性 Button.Click,并使用处理程序名作为属性值,为子元素按钮的 Click 事件注册一个处理程序。

    x:Name
    默认情况下,通过处理对象元素而创建的对象实例没有可供您在代码中使用的唯一标识符或固有的对象引用。当您在代码中调用构造函数时,几乎总是使用构造函数结果为构造的实例设置一个变量,以便以后在代码中引用该实例。为了对通过标记定义创建的对象进行标准化访问,XAML 定义了 x:Name 属性。您可以在任何对象元素上设置 x:Name 属性的值。在代码隐藏文件中,您选择的标识符等效于引用所构造的实例的实例变量。在任何方面,命名元素都像它们是对象实例一样工作(此名称只是引用该实例),并且代码隐藏文件可以引用该命名元素来处理应用程序内的运行时交互。

    WPF 框架级 XAML 元素继承 Name 属性 (Property),该属性等效于 XAML 定义的 x:Name 属性 (Attribute)。其他某些类也为 x:Name(通常也定义为 Name 属性)提供属性级等效项。一般而言,如果您在所选元素的成员表中找不到 Name 属性,可以改用 x:Name。

    下面的示例在 StackPanel 元素上设置 Name。然后,该 StackPanel 中的 Button 上的处理程序通过由 Name 设置的实例引用 buttonContainer 来引用 StackPanel。

    就像变量一样,实例的名称受范围概念的控制,因此可以在可预测的某个范围内强制名称唯一。定义页面的主要标记表示一个唯一的名称范围,而该名称范围的边界就是该页面的根元素。但是,其他标记源(如样式或样式中的模板)可以在运行时与页面交互,这种标记源常常具有其自己的名称范围,这些名称范围不一定与页面的名称范围相连接。

    附加属性和附加事件
    XAML 指定了一个语言功能,该功能允许在任何元素上指定某些属性或事件,而不管要为其设置属性或事件的元素的成员表中是否存在该属性或元素。该功能的属性版本称为附加属性,事件版本称为附加事件。从概念上讲,您可以将附加属性和附加事件认为是可以在任何元素/类上设置的全局成员,而不管其类层次结构如何。

    通常通过属性 (Attribute) 语法来使用 XAML 中的附加属性 (Property)。在属性 (Attribute) 语法中,您可以按照所有者类型.属性名 的形式指定附加属性 (Property)。表面上,这与属性元素用法类似,但在这种情况下,您指定的所有者类型 始终是一种与要为其设置附加属性的对象元素不同的类型。所有者类型 这种类型提供 XAML 处理器获取或设置附加属性值所需要的访问器方法。使用附加属性的最常见方案是使子元素能够向其父元素报告属性值。

    下面的示例演示了 DockPanel..::.Dock 附加属性。DockPanel 类为 DockPanel..::.Dock 定义访问器,因此拥有附加属性。DockPanel 类还包括一个逻辑,该逻辑迭代其子元素并具体检查每个元素是否具有 DockPanel..::.Dock 设置值。如果找到一个值,将在布局过程中使用该值定位子元素。使用 DockPanel..::.Dock 附加属性和这种定位功能事实上是 DockPanel 类的激动人心的一面。

    在 Windows Presentation Foundation (WPF) 中,所有附加属性还作为依赖项属性来实现。

    附加事件使用类似的所有者类型.事件名 属性语法形式。就像非附加事件一样,XAML 中的附加事件的属性值指定在元素上处理事件时调用的处理程序方法的名称。

    使用附加事件的一种方案适用于可在任何元素(如鼠标按钮)上处理的设备输入事件。例如,Mouse..::.MouseDown 就是这样一个附加事件。但是,大多数 WPF 框架级元素可以使用此事件,而无需使用附加事件。这是因为基元素类 UIElement 可为 Mouse..::.MouseDown 附加事件创建一个别名,并在 UIElement 成员表中公开该别名(为 MouseDown)。因此,通常不需要在 XAML 页或 Windows Presentation Foundation (WPF) 应用程序编程中指定附加事件语法。例外情况包括,您使用的是自定义元素,或者使用并非从 UIElement 派生但仍然具有可视化表示形式的对象元素(这些情况很少见)。在 WPF 中,所有附加事件还作为路由事件来实现。ContentElement 也为输入事件公开别名,供流程文档模型使用。

    XAML 页面根元素剖析
    下表显示了一个典型的 XAML 页面根元素分解结构,并显示了本主题中介绍的根元素的具体属性:

    基类和 XAML
    基础 XAML 及其架构是一个类集合,这些类对应于 CLR 对象以及要在 XAML 中使用的标记元素。但是,并不是所有的类都能映射到元素。抽象类(如 ButtonBase)和某些非抽象基类在 CLR 对象模型中用于继承,并且不支持对应的 XAML 标记。基类(包括抽象类)对于 XAML 开发仍然很重要,因为每个具体的 XAML 元素都从其层次结构中的某个基类继承成员。通常,这些成员包括可以设置为元素属性 (Attribute) 的属性 (Property),或者可以处理的事件。FrameworkElement 是 WPF 在 WPF 框架级的具体 UI 基类。设计 UI 时,您将使用各种形状、面板、修饰器或控件类,它们全部从 FrameworkElement 派生而来。有一个相关的基类 FrameworkContentElement,它使用可在 FrameworkElement 中特意镜像 API 的 API,支持适合流布局表示形式的面向文档的元素。元素级的属性 (Attribute) 和 CLR 对象模型的组合提供了一组通用的属性 (Property),可以在大多数具体的 XAML 元素上设置这些属性 (Property),而不管确切的元素类型及其基础类是什么。

    XAML 安全性
    XAML 是一种直接表示对象实例化和执行的标记语言。因此,使用 XAML 创建的元素能够像等效的生成代码那样与系统资源进行交互(如网络访问、文件系统 IO)。

    WPF 支持 .NET 安全框架代码访问安全性 (CAS)。这意味着在 Internet 区域中运行的 WPF 内容具有更少的执行权限。“松散 XAML”(由 XAML 查看器在加载时解释的未编译 XAML 的页面)和 XAML 浏览器应用程序 (XBAP) 通常在此 Internet 区域中运行,并且使用相同的权限集。但是,加载到完全受信任的应用程序中的 XAML 与宿主应用程序具有相同的系统资源访问权限。有关更多信息,请参见 Windows Presentation Foundation 部分信任安全性。

    从代码中加载 XAML
    XAML 可用于定义整个 UI,但有时也适合只使用 XAML 定义 UI 的一部分。利用此功能可以实现部分自定义、在本地存储信息、使用 XAML 提供业务对象或者各种可能的方案。这些方案的关键是 XamlReader 类及其 Load 方法。输入是一个 XAML 文件,而输出是一个对象,它表示从该标记中创建的对象的整个运行时树。然后您可以插入该对象,作为应用程序中已存在的另一个对象的属性。只要该属性在具有最终显示功能并且将通知执行引擎已在应用程序中添加新内容的内容模型中是一个合适的属性,您就可以通过以 XAML 形式加载来轻松地修改正在运行的应用程序的内容。请注意,通常只在完全受信任的应用程序中使用此功能,因为将文件加载到正在运行的应用程序中会带来明显的安全隐患。

    接下来的内容
    本主题简单介绍了 XAML 语法概念和术语。

    如果您尚未了解这些内容,请尝试阅读 Windows Presentation Foundation 入门教程。当您真正创建本教程中介绍的标记应用程序时,其中的练习将帮助您进一步了解本主题中介绍的许多概念。

    WPF 使用一个特定的应用程序模型,该模型基于 Application 类。

    生成 WPF 应用程序 (WPF) 为您详细介绍了如何通过命令行以及使用 Microsoft Visual Studio 生成包含 XAML 的应用程序。

    依赖项属性概述详细介绍了 Windows Presentation Foundation (WPF) 中属性的多样性,并介绍了依赖项属性的概念。

    最后,SDK 中还包含一个称为 XAMLPad 的 XAML 编辑工具。您可以使用此工具实时体验 XAML。

    1. WPF基础之基元素

    Windows Presentation Foundation (WPF) 中的大部分类都从四个类派生而来,这四个类在 SDK 文档中常常被称为基元素类。这些类包括 UIElement、FrameworkElement、ContentElement 和 FrameworkContentElement。DependencyObject 也是一个相关类,因为它是 UIElement 和 ContentElement 的通用基类。

    WPF 类中的基元素 API
    UIElement 和 ContentElement 都是从 DependencyObject 派生而来,但途径略有不同。此级别上的拆分涉及到 UIElement 或 ContentElement 如何在用户界面上使用,以及它们在应用程序起到什么作用。UIElement 在其类层次结构中也有 Visual,该类为 Windows Presentation Foundation (WPF) 公开较低级别的图形支持。Visual 通过定义独立的矩形屏幕区域来提供呈现框架。实际上,UIElement 适用于支持大型数据模型的元素,这些元素用于在可以称为矩形屏幕区域的区域内进行呈现和布局,在该区域内,内容模型特意设置得更加开放,以允许不同的元素进行组合。ContentElement 不是从 Visual 派生的;它的模型由其他对象(例如,阅读器或查看器,用来解释元素并生成完整的 Visual 供 Windows Presentation Foundation (WPF) 使用)来使用 ContentElement。某些 UIElement 类可用作内容宿主:它们为一个或多个 ContentElement 类(如 DocumentViewer)提供宿主和呈现。ContentElement 用作以下元素的基类:所具有的对象模型较小,并且多用于寻址可能宿主在 UIElement 中的文本、信息或文档内容。

    框架级和核心级
    UIElement 用作 FrameworkElement 的基类,ContentElement 用作 FrameworkContentElement 的基类。对于此下一级类,原因是要支持与 WPF 框架级相分离的 WPF 核心级,这种分离还存在于 API 如何在 PresentationCore 和 PresentationFramework 程序集之间进行划分。WPF 框架级为基本应用程序需要提供了一个更完整的解决方案,包括用于表示的布局管理器的实现。WPF 核心级提供了一种方法,以充分利用 WPF,而又不至于产生附加程序集开销。对于大多数典型的应用程序开发方案而言,这些级别之间的区别很少有影响,而且一般情况下应将 WPF API 视为一个整体,而无需担心 WPF 框架级与 WPF 核心级之间有何区别。如果您的应用程序设计选择替换大量 WPF 框架级功能,例如,如果您的整体解决方案已经有其自己的用户界面 (UI) 组合和布局实现,则可能需要了解级别之间的差异。

    选择从哪个元素派生
    创建用于扩展 WPF 的自定义类的最实用方法是从某个 WPF 类中派生,这样您可以通过现有的类层次结构获得尽可能多的所需功能。本节列出了三个最重要的元素类附带的功能,以帮助您决定要从哪个类进行派生。

    如果您要实现控件(这的确是从 WPF 类派生的更常见的原因之一),您可能需要从以下类中派生:实际控件、控件系列基类或至少是 Control 基类。

    如果您不是创建控件,并且需要从层次结构中较高的类进行派生,则可以参考下列各节的内容,了解每个基元素类定义了哪些特征。

    如果您创建从 DependencyObject 派生的类,则将继承以下功能:

    GetValue 和 SetValue 支持以及一般的属性系统支持。

    使用依赖项属性以及作为依赖项属性实现的附加属性的能力。

    如果您创建从 UIElement 派生的类,则除了能够继承 DependencyObject 提供的功能外,还将继承以下功能:

    对动画属性值的基本支持。

    对基本输入事件和命令的支持。

    可以重写以便为布局系统提供信息的虚方法。

    如果您创建从 FrameworkElement 派生的类,则除了能够继承 UIElement 提供的功能外,还将继承以下功能:

    对样式设置和演示图板的支持。

    对数据绑定的支持。

    对动态资源引用的支持。

    对属性值继承以及元数据中有助于向框架服务报告属性的相关情况(如数据绑定、样式或布局的框架实现)的其他标志的支持。

    逻辑树的概念。

    对布局系统的实际 WPF 框架级实现的支持,包括 OnPropertyChanged 重写(该重写可以检测到影响布局的属性更改)。

    如果您创建从 ContentElement 派生的类,则除了能够继承 DependencyObject 提供的功能外,还将继承以下功能:

    对动画的支持。

    对基本输入事件和命令的支持。

    如果您创建从 FrameworkContentElement 派生的类,则除了能够继承 ContentElement 提供的功能外,还将获得以下功能:

    对样式设置和演示图板的支持。

    对数据绑定的支持。

    对动态资源引用的支持。

    对属性值继承以及元数据中有助于向框架服务报告属性情况(如数据绑定、样式或布局的框架实现)的其他标志的支持。

    您不会继承对布局系统修改(如 ArrangeOverride)的访问权限。布局系统实现只在 FrameworkElement 上提供。但是,您会继承 OnPropertyChanged 重写(可以检测影响布局的属性更改并将这些更改报告给任何内容宿主)。

    记录了各种类的内容模型。如果您要找到一个合适的类以便从该类进行派生,其内容模型是一个应该考虑的可能因素。

    其他基类

    DispatcherObject
    DispatcherObject 为 WPF 线程模型提供支持,并允许为 WPF 应用程序创建的所有对象与 Dispatcher 相关联。即使您不从 UIElement, DependencyObject 或 Visual 派生,也应考虑从 DispatcherObject 派生,以获得此线程模型支持。

    Visual
    Visual 实现二维对象在近似矩形的区域中通常需要具有可视化表示的概念。Visual 的实际呈现发生在其他类中(不是独立的),但是 Visual 类提供了一个由各种级别的呈现处理使用的已知类型。Visual 实现命中测试,但它不公开报告命中测试结果的事件(这些都位于 UIElement 中)。

    Freezable
    Freezable 通过在出于性能原因需要不可变对象时提供为对象生成副本的途径,来模拟可变对象的不变性。Freezable 类型为某些图形元素(如几何形状、画笔以及动画)提供了一个通用的基础。值得注意的是,Freezable 不是一个 Visual;当应用 Freezable 以填充另一个对象的属性值时,它包含的属性将变成子属性,而这些子属性可能会影响呈现。

    Animatable
    Animatable 是一个 Freezable 派生类,它特别添加了动画控件层和某些实用工具成员,从而使当前动画的属性可以与未动画的属性区分开。

    Control
    Control 是称为控件或组件(取决于技术)的对象类型的理想基类。一般而言,WPF 控件类是直接表示 UI 控件或积极参与控件组合的类。Control 实现的主要功能是控件模板化。

    1. WPF基础之属性系统

    Windows Presentation Foundation (WPF) 提供了一组服务,这些服务可用于扩展公共语言运行库 (CLR) 属性的功能。这些服务通常统称为 WPF 属性系统。由 WPF 属性系统支持的属性称为依赖项属性。本概述介绍 WPF 属性系统以及依赖项属性的功能,这包括如何在可扩展应用程序标记语言 (XAML) 中和代码中使用现有的依赖项属性。本概述还介绍了依赖项属性所特有的方面(如依赖项属性元数据),并说明了如何在自定义类中创建自己的依赖项属性。

    先决条件
    本主题假设您在 CLR 和面向对象的编程方面有一些基础知识。若要采用本主题中的示例,还应当了解 XAML 并知道如何编写 WPF 应用程序。

    依赖项属性和 CLR 属性
    在 WPF 中,属性通常公开为公共语言运行库 (CLR) 属性。在基本级别,您可以在根本不知道这些属性实现为依赖项属性的情况下直接与它们交互。但是,您应当熟悉 WPF 属性系统的部分或全部功能,才能利用这些功能。

    依赖项属性的用途在于提供一种方法来基于其他输入的值计算属性值。这些其他输入可以包括系统属性(如主题和用户首选项)、实时属性确定机制(如数据绑定和动画/演示图板)、重用模板(如资源和样式)或者通过与元素树中其他元素的父子关系来公开的值。另外,可以通过实现依赖项属性来提供独立验证、默认值、监视其他属性的更改的回调以及可以基于可能的运行时信息来强制指定属性值的系统。派生类还可以通过重写依赖项属性元数据(而不是重写现有属性的实际实现或者创建新属性)来更改现有属性的某些具体特征。

    在 SDK 参考中,可以根据某个属性的托管引用页上是否存在“依赖项属性信息”部分来确定该属性是否为依赖项属性。“依赖项属性信息”部分包括一个指向该依赖项属性的 DependencyProperty 标识符字段的链接,还包括一个为该属性设置的元数据选项的列表、每个类的重写信息以及其他详细信息。

    依赖项属性支持 CLR 属性
    依赖项属性和 WPF 属性系统通过提供一个支持属性的类型来扩展属性功能,这是使用私有字段支持该属性的标准模式的替代实现方法。该类型的名称是 DependencyProperty。定义 WPF 属性系统的另一个重要类型是 DependencyObject。DependencyObject 定义可以注册和拥有依赖项属性的基类。

    下面汇集了在本软件开发工具包 (SDK) 文档中,在讨论依赖项属性时所使用的术语:

    依赖项属性:一个由 DependencyProperty 支持的属性。

    依赖项属性标识符:一个 DependencyProperty 实例,在注册依赖项属性时作为返回值获得,之后将存储为一个类成员。在与 WPF 属性系统交互的许多 API 中,此标识符用作一个参数。

    CLR“包装”:属性的实际 get 和 set 实现。这些实现通过在 GetValue 和 SetValue 调用中使用依赖项属性标识符来合并此标识符,从而使用 WPF 属性系统为属性提供支持。

    下面的示例定义 IsSpinning 依赖项属性,并说明 DependencyProperty 标识符与它所支持的属性之间的关系。

    属性以及支持它的 DependencyProperty 字段的命名约定非常重要。字段总是与属性同名,但其后面追加了 Property 后缀。

    设置属性值
    可以在代码或 XAML 中设置属性。

    在 XAML 中设置属性值
    下面的 XAML 示例将按钮的背景色指定为红色。该示例演示了这样一种情况:在所生成的代码中,XAML 加载器将 XAML 属性的简单字符串值的类型转换为 WPF 类型(一种 Color,通过 SolidColorBrush)。

    XAML 支持各种设置属性的语法格式。要对特定的属性使用哪种语法取决于该属性所使用的值类型以及其他因素(例如,是否存在类型转换器)。

    作为非属性语法的示例,下面的 XAML 示例显示了另一种按钮背景。这一次不是设置简单的纯色,而是将背景设置为图像,用一个元素表示该图像并将该图像的源指定为嵌套元素的属性。这是属性元素语法的示例。

    在代码中设置属性
    在代码中设置依赖项属性值通常只是调用由 CLR“包装”公开的 set 实现。

    获取属性值实质上也是在调用 get“包装”实现:

    您还可以直接调用属性系统 API GetValue 和 SetValue。如果您使用的是现有属性,则上述操作通常不是必需的(使用包装会更方便,并能够更好地向开发人员工具公开属性)。但是在某些情况下适合直接调用 API。

    还可以在 XAML 中设置属性,然后通过代码隐藏在代码中访问这些属性。

    由依赖项属性提供的属性功能
    依赖项属性提供用来扩展属性功能的功能,这与字段支持的属性相反。每个这样的功能通常都表示或支持整套 WPF 功能中的特定功能:

    资源

    数据绑定

    样式

    动画

    元数据重写

    属性值继承

    WPF 设计器集成

    资源
    依赖项属性值可以通过引用资源来设置。资源通常指定为页面根元素或应用程序的子元素,通过这些位置可以最方便地访问资源。下面的示例演示如何定义 SolidColorBrush 资源。

    在定义了某个资源之后,可以引用该资源并使用它来提供属性值:

    这个特定的资源称为 DynamicResource 标记扩展(在 XAML 中,可以使用静态或动态资源引用)。若要使用动态资源引用,必须设置为依赖项属性,因此它是由 WPF 属性系统明确启用的动态资源引用用法。

    说明:
    资源被视为本地值,这意味着,如果您设置另一个本地值,该资源引用将被消除。
     
    数据绑定
    依赖项属性可以通过数据绑定来引用值。数据绑定通过特定的标记扩展语法(在 XAML 中)或 Binding 对象(在代码中)来工作。使用数据绑定,最终属性值的确定将延迟到运行时,在运行时,将从数据源获取属性值。

    下面的示例在 XAML 中使用一个绑定,为 Button 设置 Content 属性。该绑定使用一个继承的数据上下文和一个 XmlDataProvider 数据源(未显示出来)。绑定本身通过数据源中的 XPath 指定所需的源属性。

     

    绑定被视为本地值,这意味着,如果您设置另一个本地值,该绑定将被消除。

    依赖项属性或 DependencyObject 类本身并不支持 INotifyPropertyChanged,以便为数据绑定操作生成有关 DependencyObject 源属性值变化的通知。

    样式
    样式和模板是使用依赖项属性的两个主要激发方案。在设置定义应用程序用户界面 (UI) 的属性时,样式尤其有用。样式在 XAML 中通常定义为资源。样式与属性系统交互,因为它们通常包含特定属性的“setter”,以及基于另一个属性的实时值更改属性值的“trigger”。

    下面的示例创建一个非常简单的样式(该样式将在 Resources 字典中定义,未显示出来),然后将该样式直接应用于 Button 的 Style 属性。样式中的 setter 将带样式的 Button 的 Background 属性设置为 green。

     

    动画
    可以对依赖项属性进行动画处理。在应用和运行动画时,经过动画处理的值的操作优先级将高于该属性以其他方式具有的任何值(如本地值)。

    下面的示例对 Button 属性的 Background 进行动画处理(在技术上,Background 是通过使用属性元素语法将空白 SolidColorBrush 指定为 Background 来进行动画处理的,之后,该 SolidColorBrush 的 Color 属性就是直接进行动画处理的属性)。

     

    元数据重写
    在从最初注册依赖项属性的类派生时,可以通过重写依赖项属性的元数据来更改该属性的某些行为。对元数据的重写依赖于 DependencyProperty 标识符。重写元数据不需要重新实现属性。元数据的变化是由属性系统在本机处理的;对于所有从基类继承的属性,每个类都有可能基于每个类型保留元数据。

    下面的示例重写依赖项属性 DefaultStyleKey 的元数据。重写这个特定的依赖项属性的元数据是某个实现模式的一部分,该模式创建可以使用主题中的默认样式的控件。

     

    属性值继承
    元素可以从其在树中的父级继承依赖项属性的值。

    说明:
    属性值继承行为并未针对所有的依赖项属性在全局启用,因为继承的计算时间确实会对性能产生一定的影响。属性值继承通常只有在特定方案指出适合使用属性值继承时才对属性启用。可以通过在 SDK 参考中查看某个依赖项属性的“依赖项属性信息”部分,来确定该依赖项属性是否继承属性值。

    下面的示例演示一个绑定,并设置指定绑定(在前面的绑定示例中未显示出来)的源的 DataContext 属性。DataContext 属性的值继承,因此子元素中的任何后续绑定都不必遵守在父级 StackPanel 元素中指定为 DataContext 的源。

     

    WPF 设计器集成
    如果自定义控件具有实现为依赖项属性的属性,则它将收到相应的 Visual Studio Windows Presentation Foundation (WPF) 设计器支持。一个示例就是能够在“属性”窗口中编辑直接依赖项属性和附加依赖项属性。
    依赖项属性值优先级
    当您获取依赖项属性的值时,可能会获得通过其他参与 WPF 属性系统且基于属性的任一输入而在该属性上设置的值。由于存在依赖项属性值优先级,使得属性获取值的方式的各种方案得以按可预测的方式交互。

    请看下面的示例。该示例包括一个应用于所有按钮及其 Background 属性的样式,但是之后还指定了一个具有在本地设置的 Background 值的按钮。

    说明:
    SDK 文档在讨论依赖项属性时有时会使用“本地值”或“本地设置的值”等术语。本地设置的值是指在代码中直接为对象实例设置的属性 (Property) 值,或者在 XAML 中设置为元素属性 (Attribute) 的属性 (Property) 值。 

    实际上,对于第一个按钮,该属性设置了两次,但是仅应用了一个值,即,具有最高优先级的值。本地设置的值具有最高优先级(对于正在运行的动画除外,但是在本示例中没有应用动画),因此,对于第一个按钮的背景将使用本地设置的值,而不使用样式 setter 值。第二个按钮没有本地值(而且没有其他比样式 setter 优先级更高的值),因此该按钮中的背景将来自样式 setter。

     

    为什么存在依赖项属性优先级?
    通常,您不会希望总是应用样式,而且不希望样式遮盖单个元素的哪怕一个本地设置值(否则,通常将很难使用样式或元素)。因此,来自样式的值的操作优先级将低于本地设置的值。
    说明:
    在 WPF 元素定义了许多非依赖项属性的属性。一般说来,只有在需要支持至少一个由属性系统启用的方案(数据绑定、样式、动画、默认值支持、继承、附加属性或失效)时,才将属性实现为依赖项属性。

    了解有关依赖项属性的更多信息
    附加属性是一种类型的属性,它支持 XAML 中的专用语法。附加属性通常与公共语言运行库 (CLR) 属性不具有 1:1 对应关系,而且不一定是依赖项属性。附加属性的典型用途是使子元素可以向其父元素报告属性值,即使父元素和子元素的类成员列表中均没有该属性也是如此。一个主要方案是,使子元素可以将其在 UI 中的表示方式通知给父级。
    组件开发人员或应用程序开发人员可能希望创建自己的依赖项属性,以便实现数据绑定或样式支持之类的功能,或者实现对失效和强制指定值的支持。
    通常,依赖项属性应当被视为公共属性,这些公共属性可以由任何具有实例访问权限的调用方访问,或至少可被这样的调用方发现。

    1. WPF基础之路由事件

    本主题描述 Windows Presentation Foundation (WPF) 中路由事件的概念。本主题定义路由事件术语,描述路由事件如何通过元素树来路由,概述如何处理路由事件,并介绍如何创建您自己的自定义路由事件。

    先决条件
    本主题假设您对如下内容有基本的了解:公共语言运行库 (CLR)、面向对象的编程以及如何用树的概念来说明 WPF 元素之间的关系。为了按照本主题中的示例操作,您还应当了解可扩展应用程序标记语言 (XAML) 并知道如何编写非常基本的 WPF 应用程序或页。
    什么是路由事件?
    可以从功能或实现的角度来考虑路由事件。此处对这两种定义均进行了说明,因为用户当中有的认为前者更有用,而有的则认为后者更有用。

    功能定义:路由事件是一种可以针对元素树中的多个侦听器(而不是仅针对引发该事件的对象)调用处理程序的事件。

    实现定义:路由事件是一个 CLR 事件,可以由 RoutedEvent 类的实例提供支持并由 Windows Presentation Foundation (WPF) 事件系统来处理。

    典型的 WPF 应用程序中包含许多元素。无论这些元素是在代码中创建的还是在 XAML 中声明的,它们都由共同所在的元素树关联起来。根据事件的定义,事件路由可以按两种方向之一传播,但是通常会在元素树中从源元素向上“冒泡”,直到它到达元素树的根(通常是页面或窗口)。如果您以前用过 DHTML 对象模型,则可能会熟悉这个冒泡概念。

    请考虑下面的简单元素树:

    此元素树生成类似如下的内容:


    在这个简化的元素树中,Click 事件的源是某个 Button 元素,而所单击的 Button 是有机会处理该事件的第一个元素。但是,如果附加到 Button 的任何处理程序均未作用于该事件,则该事件将向上冒泡到元素树中的 Button 父级(即 StackPanel)。该事件可能会冒泡到 Border,然后会到达元素树的页面根(未显示出来)。

    换言之,此 Click 事件的事件路由为:

    Button-->StackPanel-->Border-->...

    路由事件的顶级方案
    下面简要概述了需运用路由事件的方案,以及为什么典型的 CLR 事件不适合这些方案:

    控件的撰写和封装:WPF 中的各个控件都有一个丰富的内容模型。例如,可以将图像放在 Button 的内部,这会有效地扩展按钮的可视化树。但是,所添加的图像不得中断命中测试行为(该行为会使按钮响应对图像内容的 Click),即使用户所单击的像素在技术上属于该图像也是如此

    单一处理程序附加点:在 Windows 窗体中,必须多次附加同一个处理程序,才能处理可能是从多个元素引发的事件。路由事件使您可以只附加该处理程序一次(像上例中那样),并在必要时使用处理程序逻辑来确定该事件源自何处。例如,这可以是前面显示的 XAML 的处理程序:

    类处理:路由事件允许使用由类定义的静态处理程序。这个类处理程序能够抢在任何附加的实例处理程序之前来处理事件。

    引用事件,而不反射:某些代码和标记技术需要能标识特定事件的方法。路由事件创建 RoutedEvent 字段作为标识符,以此提供不需要静态反射或运行时反射的可靠的事件标识技术。

    路由事件的实现方式
    路由事件是一个 CLR 事件,它由 RoutedEvent 类提供支持并用 WPF 事件系统注册。从注册中获取的 RoutedEvent 实例通常保留为某种类的 public static readonly 字段成员,该类进行了注册并因此“拥有”路由事件。与同名 CLR 事件(有时称为“包装”事件)的连接是通过重写 CLR 事件的 add 和 remove 实现来完成的。通常,add 和 remove 保留为隐式默认值,该默认值使用特定于语言的相应事件语法来添加和移除该事件的处理程序。路由事件的支持和连接机制在概念上与以下机制相似:依赖项属性是一个 CLR 属性,该属性由 DependencyProperty 类提供支持并用 WPF 属性系统注册。

    下面的示例演示自定义 Tap 路由事件的声明,其中包括注册和公开 RoutedEvent 标识符字段以及对 Tap CLR 事件进行 add 和 remove 实现。

    路由事件处理程序和 XAML
    若要使用 XAML 为某个事件添加处理程序,请将该事件的名称声明为用作事件侦听器的元素上的属性。该属性的值是所实现的处理程序方法的名称,该方法必须存在于代码隐藏文件的分部类中。

    用来添加标准 CLR 事件处理程序的 XAML 语法与用来添加路由事件处理程序的语法相同,因为您实际上是在向下面具有路由事件实现的 CLR 事件包装中添加处理程序。

    路由策略

    路由事件使用以下三个路由策略之一:

    冒泡:针对事件源调用事件处理程序。路由事件随后会路由到后续的父元素,直到到达元素树的根。大多数路由事件都使用冒泡路由策略。冒泡路由事件通常用来报告来自不同控件或其他 UI 元素的输入或状态变化。

    直接:只有源元素本身才有机会调用处理程序以进行响应。这与 Windows 窗体用于事件的“路由”相似。但是,与标准 CLR 事件不同的是,直接路由事件支持类处理(类处理将在下一节中介绍)而且可以由 EventSetter 和 EventTrigger 使用。

    隧道:最初将在元素树的根处调用事件处理程序。随后,路由事件将朝着路由事件的源节点元素(即引发路由事件的元素)方向,沿路由线路传播到后续的子元素。在合成控件的过程中通常会使用或处理隧道路由事件,这样,就可以有意地禁止显示复合部件中的事件,或者将其替换为特定于整个控件的事件。在 WPF 中提供的输入事件通常是以隧道/冒泡对实现的。隧道事件有时又称作 Preview 事件,这是由隧道/冒泡对所使用的命名约定决定的。

    为什么使用路由事件?
    作为应用程序开发人员,您不需要始终了解或关注正在处理的事件是否作为路由事件实现。路由事件具有特殊的行为,但是,如果您在引发该行为的元素上处理事件,则该行为通常会不可见。

    如果您使用以下任一建议方案,路由事件的功能将得到充分发挥:在公用根处定义公用处理程序、合成自己的控件或者定义您自己的自定义控件类。

    路由事件侦听器和路由事件源不必在其层次结构中共享公用事件。任何 UIElement 或 ContentElement 可以是任一路由事件的事件侦听器。因此,您可以使用在整个工作 API 集内可用的全套路由事件作为概念“接口”,应用程序中的不同元素凭借这个接口来交换事件信息。路由事件的这个“接口”概念特别适用于输入事件。

    路由事件还可以用来通过元素树进行通信,因为事件的事件数据会永存到路由中的每个元素中。一个元素可以更改事件数据中的某项内容,该更改将对于路由中的下一个元素可用。

    之所以将任何给定的 WPF 事件作为路由事件实现(而不是作为标准 CLR 事件实现),除了路由方面的原因,还有两个其他原因。如果您要实现自己的事件,则可能也需要考虑这两个因素:

    某些 WPF 样式和模板功能(如 EventSetter 和 EventTrigger)要求所引用的事件是路由事件。前面提到的事件标识符方案就是这样的。

    路由事件支持类处理机制,类可以凭借该机制来指定静态方法,这些静态方法能够在任何已注册的实例程序访问路由事件之前,处理这些路由事件。这在控件设计中非常有用,因为您的类可以强制执行事件驱动的类行为,以防它们在处理实例上的事件时被意外禁止。

    本主题将用单独的章节来讨论每个考虑因素。

    为路由事件添加和实现事件处理程序
    若要在 XAML 中添加事件处理程序,只需将相应的事件名称作为一个属性添加到某个元素中,并将该属性的值设置为用来实现相应委托的事件处理程序的名称,如下面的示例中所示。

    b1SetColor 是所实现的处理程序的名称,该处理程序中包含用来处理 Click 事件的代码。b1SetColor 必须具有与 RoutedEventHandler 委托相同的签名,该委托是 Click 事件的事件处理程序委托。所有路由事件处理程序委托的第一个参数都指定要向其中添加事件处理程序的元素,第二个参数指定事件的数据。

    RoutedEventHandler 是基本的路由事件处理程序委托。对于针对某些控件或方案而专门处理的路由事件,要用于路由事件处理程序的委托还可能会变得更加专用化,因此它们可以传输专用的事件数据。例如,在常见的输入方案中,可以处理 DragEnter 路由事件。您的处理程序应当实现 DragEventHandler 委托。借助更具体的委托,可以对处理程序中的 DragEventArgs 进行处理,并读取 Data 属性,该属性中包含拖动操作的剪贴板内容。

    在用代码创建的应用程序中为路由事件添加处理程序非常简单。路由事件处理程序始终可以通过帮助器方法 AddHandler 来添加,现有支持为 add 调用的也是此方法。但是,现有的 WPF 路由事件通常借助于支持机制来实现 add 和 remove 逻辑,这些逻辑允许使用特定于语言的事件语法来添加路由事件的处理程序,特定于语言的事件语法比帮助器方法更直观。下面是帮助器方法的示例用法:

    下一个示例演示 C# 运算符语法(visual Basic 的运算符语法稍有不同,因为它以不同的方法来处理取消引用):

    如果使用的是 Visual Basic,则还可以使用 Handles 关键字,将处理程序作为处理程序声明的一部分来添加。

    已处理”概念
    所有的路由事件都共享一个公用的事件数据基类 RoutedEventArgs。RoutedEventArgs 定义了一个采用布尔值的 Handled 属性。Handled 属性的目的在于,允许路由中的任何事件处理程序通过将 Handled 的值设置为 true 来将路由事件标记为“已处理”。处理程序在路由路径上的某个元素处对共享事件数据进行处理之后,这些数据将再次报告给路由路径上的每个侦听器。

    Handled 的值影响路由事件在沿路由线路向远处传播时的报告或处理方式。在路由事件的事件数据中,如果 Handled 为 true,则通常不再为该特定事件实例调用负责在其他元素上侦听该路由事件的处理程序。这条规则对以下两类处理程序均适用:在 XAML 中附加的处理程序;由语言特定的事件处理程序附加语法(如 += 或 Handles)添加的处理程序。对于最常见的处理程序方案,如果将 Handled 设置为 true,以此将事件标记为“已处理”,则将“停止”隧道路由或冒泡路由,同时,类处理程序在某个路由点处处理的所有事件的路由也将“停止”。

    但是,侦听器仍可以凭借“handledEventsToo”机制来运行处理程序,以便在事件数据中的 Handled 为 true 时响应路由事件。换言之,将事件数据标记为“已处理”并不会真的停止事件路由。您只能在代码或 EventSetter 中使用 handledEventsToo 机制:

    在代码中,不使用适用于一般 CLR 事件的特定于语言的事件语法,而是通过调用 WPF 方法 AddHandler(RoutedEvent, Delegate, Boolean) 来添加处理程序。使用此方法时,请将 handledEventsToo 的值指定为 true。

    在 EventSetter 中,请将 HandledEventsToo 属性设置为 true。

    除了 Handled 状态在路由事件中生成的行为以外,Handled 概念还暗示您应当如何设计自己的应用程序和编写事件处理程序代码。可以将 Handled 概念化为由路由事件公开的简单协议。此协议的具体使用方法由您来决定,但是需要按照如下方式来对 Handled 值的预期使用方式进行概念设计:

    如果路由事件标记为“已处理”,则它不必由该路由中的其他元素再次处理。

    如果路由事件未标记为“已处理”,则说明该路由中前面的其他侦听器已经选择了不注册处理程序,或者已经注册的处理程序选择不操作事件数据并将 Handled 设置为 true。(或者,当前的侦听器很可能就是路由中的第一个点。) 当前侦听器上的处理程序现在有三个可能的操作方案:

    不执行任何操作;该事件保持未处理状态,该事件将路由到下一个侦听器。

    执行代码以响应该事件,但是所执行的操作被视为不足以保证将事件标记为“已处理”。该事件将路由到下一个侦听器。

    执行代码以响应该事件。在传递到处理程序的事件数据中将该事件标记为“已处理”,因为所执行的操作被视为不足以保证将该事件标记为“已处理”。该事件仍将路由到下一个侦听器,但是,由于其事件数据中存在 Handled=true,因此只有 handledEventsToo 侦听器才有机会调用进一步的处理程序。

    这个概念设计是通过前面提到的路由行为来加强的:即使路由中前面的处理程序已经将 Handled 设置为 true,也会增加为所调用的路由事件附加处理程序的难度(尽管仍可以在代码或样式中实现这一目的)。

    有关 Handled、路由事件的类处理的更多信息,以及针对何时适合将路由事件标记为 Handled 的建议,请参见将路由事件标记为已处理以及类处理。

    在应用程序中,相当常见的做法是只针对引发冒泡路由事件的对象来处理该事件,而根本不考虑事件的路由特征。但是,在事件数据中将路由事件标记为“已处理”仍是一个不错的做法,因为这样可以防止元素树中位置更高的元素也对同一个路由事件附加了处理程序而出现意外的副作用。

    类处理程序
    如果您定义的类是以某种方式从 DependencyObject 派生的,那么对于作为类的已声明或已继承事件成员的路由事件,还可以定义和附加一个类处理程序。每当路由事件到达其路由中的元素实例时,都会先调用类处理程序,然后再调用附加到该类某个实例的任何实例侦听器处理程序。

    有些 WPF 控件对某些路由事件具有固有的类处理。路由事件可能看起来从未引发过,但实际上正对其进行类处理,如果您使用某些技术的话,路由事件可能仍由实例处理程序进行处理。同样,许多基类和控件都公开可用来重写类处理行为的虚方法。

    WPF 中的附加事件
    XAML 语言还定义了一个名为“附加事件”的特殊类型的事件。使用附加事件,可以将特定事件的处理程序添加到任意元素中。正在处理该事件的元素不必定义或继承附加事件,可能引发这个特定事件的对象和用来处理实例的目标也都不必将该事件定义为类成员或将其作为类成员来“拥有”。

    WPF 输入系统广泛地使用附加事件。但是,几乎所有的附加事件都是通过基本元素转发的。输入事件随后会显示为等效的、作为基本元素类成员的非附加路由事件。例如,通过针对该 UIElement 使用 MouseDown(而不是在 XAML 或代码中处理附加事件语法),可以针对任何给定的 UIElement 更方便地处理基础附加事件 Mouse..::.MouseDown。

    XAML 中的限定事件名称
    为子元素所引发的路由事件附加处理程序是另一个语法用法,它与类型名称.事件名称 附加事件语法相似,但它并非严格意义上的附加事件用法。可以向公用父级附加处理程序以利用事件路由,即使公用父级可能不将相关的路由事件作为其成员也是如此。请再次考虑下面的示例:

    在这里,在其中添加处理程序的父元素侦听器是 StackPanel。但是,它正在为已经声明而且将由 Button 类(实际上是 ButtonBase,但是可以由 Button 通过继承来使用)引发的路由事件添加处理程序。Button“拥有”该事件,但是路由事件系统允许将任何路由事件的处理程序附加到任何 UIElement 或 ContentElement 实例侦听器,该侦听器可能会以其他方式为公共语言运行库 (CLR) 事件附加侦听器。对于这些限定的事件属性名来说,默认的 xmlns 命名空间通常是默认的 WPF xmlns 命名空间,但是您还可以为自定义路由事件指定带有前缀的命名空间。

    WPF 输入事件
    路由事件在 WPF 平台中的常见用法之一是用于事件输入。在 WPF 中,按照约定,隧道路由事件的名称以单词“Preview”开头。输入事件通常成对出现,一个是冒泡事件,另一个是隧道事件。例如,KeyDown 事件和 PreviewKeyDown 事件具有相同的签名,前者是冒泡输入事件,后者是隧道输入事件。偶尔,输入事件只有冒泡版本,或者有可能只有直接路由版本。在本文档中,当存在具有替换路由策略的类似路由事件时,路由事件主题交叉引用它们,而且托管引用页面中的各个节阐释每个路由事件的路由策略。

    实现成对出现的 WPF 输入事件的目的在于,使来自输入的单个用户操作(如按鼠标按钮)按顺序引发该对中的两个路由事件。首先引发隧道事件并沿路由传播,然后引发冒泡事件并沿其路由传播。顾名思义,这两个事件会共享同一个事件数据实例,因为用来引发冒泡事件的实现类中的 RaiseEvent 方法调用会侦听隧道事件中的事件数据并在新引发的事件中重用它。具有隧道事件处理程序的侦听器首先获得将路由事件标记为“已处理”的机会(先是类处理程序,后是实例处理程序)。如果隧道路由中的某个元素将路由事件标记为“已处理”,则会针对冒泡事件发送已经处理的事件数据,而且将不调用为等效的冒泡输入事件附加的典型处理程序。已处理的冒泡事件看起来好像尚未引发过。此处理行为对于控件合成非常有用,因为此时您可能希望所有基于命中测试的输入事件或者所有基于焦点的输入事件都由最终的控件(而不是它的复合部件)报告。作为可支持控件类的代码的一部分,最后一个控件元素靠近合成链中的根,因此将有机会首先对隧道事件进行类处理,或许还有机会将该路由事件“替换”为更特定于控件的事件。

    为了说明输入事件处理的工作方式,请考虑下面的输入事件示例。在下面的树插图中,leaf element #2 是先后发生的 PreviewMouseDown 事件和 MouseDown 事件的源。

    输入事件的冒泡和隧道
     
    事件的处理顺序如下所示:

    针对根元素处理 PreviewMouseDown(隧道)。

    针对中间元素 1 处理 PreviewMouseDown(隧道)。

    针对源元素 2 处理 PreviewMouseDown(隧道)。

    针对源元素 2 处理 MouseDown(冒泡)。

    针对中间元素 1 处理 MouseDown(冒泡)。

    针对根元素处理 MouseDown(冒泡)。

    路由事件处理程序委托提供对以下两个对象的引用:引发该事件的对象以及在其中调用处理程序的对象。在其中调用处理程序的对象是由 sender 参数报告的对象。首先在其中引发事件的对象是由事件数据中的 Source 属性报告的。路由事件仍可以由同一个对象引发和处理,在这种情况下,sender 和 Source 是相同的(事件处理示例列表中的步骤 3 和 4 就是这样的情况)。

    由于存在隧道和冒泡,因此父元素接收 Source 作为其子元素之一的输入事件。当有必要知道源元素是哪个元素时,可以通过访问 Source 属性来标识源元素。

    通常,一旦将输入事件标记为 Handled,就将不进一步调用处理程序。通常,一旦调用了用来对输入事件的含义进行特定于应用程序的逻辑处理的处理程序,就应当将输入事件标记为“已处理”。

    对于这个有关 Handled 状态的通用声明有一个例外,那就是,注册为有意忽略事件数据 Handled 状态的输入事件处理程序仍将在其路由中被调用。

    通常,隧道事件和冒泡事件之间的共享事件数据模型以及先引发隧道事件后引发冒泡事件等概念并非对于所有的路由事件都适用。该行为的实现取决于 WPF 输入设备选择引发和连接输入事件对的具体方式。实现自己的输入事件是一个高级方案,但是您也可以选择针对自己的输入事件遵循该模型。

    一些类选择对某些输入事件进行类处理,其目的通常是重新定义用户驱动的特定输入事件在该控件中的含义并引发新事件。

           EventSetter 和 EventTrigger
    在样式中,可以通过使用 EventSetter 在标记中包括某个预先声明的 XAML 事件处理语法。在应用样式时,所引用的处理程序会添加到带样式的实例中。只能针对路由事件声明 EventSetter。下面是一个示例。请注意,此处引用的 b1SetColor 方法位于代码隐藏文件中。

    这样做的好处在于,样式有可能包含大量可应用于应用程序中任何按钮的其他信息,让 EventSetter 成为该样式的一部分甚至可以提高代码在标记级别的重用率。而且,EventSetter 还进一步从通用的应用程序和页面标记中提取处理程序方法的名称。

    另一个将 WPF 的路由事件和动画功能结合在一起的专用语法是 EventTrigger。与 EventSetter 一样,只有路由事件可以用于 EventTrigger。通常将 EventTrigger 声明为样式的一部分,但是还可以在页面级元素上将 EventTrigger 声明为 Triggers 集合的一部分或者在 ControlTemplate 中对其进行声明。使用 EventTrigger,可以指定当路由事件到达其路由中的某个元素(这个元素针对该事件声明了 EventTrigger)时将运行的 Storyboard。与只是处理事件并且会导致它启动现有演示图板相比,EventTrigger 的好处在于,EventTrigger 对演示图板及其运行时行为提供更好的控制。

    本主题主要从以下角度讨论路由事件:描述基本概念;就如何以及何时响应各种基元素和基控件中已经存在的路由事件提供指南。但是,您可以为自定义类创建自己的路由事件,还可以创建所有必要的支持(如专用的事件数据类和委托)。路由事件的所有者可以是任何类,但是路由事件只有在由 UIElement 或 ContentElement 派生类引发和处理之后才有用。

    可扩展应用程序标记语言 (XAML) 定义了一个语言组件和称为“附加事件”的事件类型。附加事件的概念允许您针对特定事件为任意元素(而不是为实际定义或继承该事件的元素)添加处理程序。在这种情况下,对象既不会引发该事件,目标处理实例也不会定义或“拥有”该事件。

    先决条件
    本主题假定您已阅读路由事件概述和 XAML 概述。

    附加事件语法
    附加事件具有一种 XAML 语法和编码模式,后备代码必须使用该语法和编码模式才支持附加事件的使用。

    在 XAML 语法中,不仅可以通过事件名称来指定附加事件,而且还可以通过用点 (.) 分隔的事件拥有类型加上事件名称来指定。因为事件名称是使用其拥有类型的名称限定的,所以附加事件语法允许将任何附加事件附加到可以实例化的任何元素上。

    例如,下面是为自定义 NeedsCleaning 附加事件附加处理程序的 XAML 语法:

    请注意 aqua: 前缀;该前缀在本例中是必需的,因为附加事件是来自自定义映射 xmlns 的自定义事件。

    如何在 WPF 中实现附加事件
    在 WPF 中,附加事件由 RoutedEvent 字段来支持,并在引发后通过元素树进行路由。通常,附加事件的源(引发该事件的对象)是系统或服务源,所以运行引发该事件的代码的对象并不是元素树的直接组成部分。

    附加事件的方案
    在 WPF 中,附加事件存在于具有服务级抽象的某些功能区域,例如,对于通过静态 Mouse 类或 Validation 类实现的事件。与服务交互或使用服务的类可以在附加事件语法中使用该事件,也可以选择将附加事件作为路由事件来实现(这是类如何集成服务功能的一部分)。

    尽管 WPF 定义了许多附加事件,但您直接使用或处理附加事件的方案却很有限。一般情况下,附加事件用于实现体系结构目的,但随后即被转发给非附加(使用 CLR 事件“包装”提供支持)路由事件。

    例如,通过针对该 UIElement 使用 MouseDown(而不是在 XAML 或代码中处理附加事件语法),可以针对任何给定的 UIElement 更方便地处理基础附加事件 Mouse..::.MouseDown。附加事件用于实现体系结构目的,因为它允许进一部扩展输入设备。假设的设备只需引发 Mouse..::.MouseDown 即可模拟鼠标输入,而不需要从 Mouse 派生。但是,此方案会涉及事件的代码处理,而附加事件的 XAML 处理则与此方案无关。

    在 WPF 中处理附加事件
    处理附加事件的过程以及您将要编写的处理程序代码与路由事件基本相同。

    一般情况下,WPF 附加事件与 WPF 路由事件并没有太大的区别。不同之处在于如何确定事件的源,以及如何通过类将事件作为成员进行公开。(这还将影响 XAML 处理程序语法。)

    但是,正如前文所述,现有的 WPF 附加事件并不是专门用于在 WPF 中进行处理。该事件的目的常常是实现合成元素,以便向合成中的父元素报告某个状态,在这种情况下,该事件常常在代码中引发,并且还依赖于相关父类中的类处理。例如,Selector 中的项应引发附加的 Selected 事件,该事件随后将由 Selector 类进行类处理,然后可能由 Selector 类转换为不同的路由事件 SelectionChanged。

    将您自己的附加事件定义为路由事件
    如果您从通用 WPF 基类派生,则可以通过在类中包括某些模式方法并使用基类中已经存在的实用工具方法来实现您自己的附加事件。

    模式如下:

    带有两个参数的方法 Add*Handler。第一个参数必须标识事件,而标识的事件必须与方法名称中带有 * 的名称相匹配。第二个参数是要添加的处理程序。该方法必须是公共且静态的,没有任何返回值。

    带有两个参数的方法 Remove*Handler。第一个参数必须标识事件,而标识的事件必须与方法名称中带有 * 的名称相匹配。第二个参数是要移除的处理程序。该方法必须是公共且静态的,没有任何返回值。

    当针对某个元素声明附加事件处理程序属性时,Add*Handler 访问器方法可以加快 XAML 处理。Add*Handler 和 Remove*Handler 方法还可实现对附加事件的事件处理程序存储区的代码访问。

    这个普通模式对于框架中的实际实现还不够精确,因为任何给定的 XAML 读取器实现都可能采用不同的架构在支持语言和体系结构中标识基础事件。这是 WPF 将附加事件作为路由事件来实现的原因之一;用于事件的标识符 (RoutedEvent) 已经由 WPF 事件系统进行定义。而且,路由一个事件也是对附加事件的 XAML 语言级概念的自然实现扩展。

    WPF 附加事件的 Add*Handler 实现包括将路由事件和处理程序用作参数来调用 AddHandler。

    此实现策略和路由事件系统一般将附加事件的处理限制为 UIElement 派生类或 ContentElement 派生类,因为只有这些类才具有 AddHandler 实现。

    例如,下面的代码按照 WPF 将附加事件作为路由事件进行声明的附加事件策略为所有者类 Aquarium 定义 NeedsCleaning 附加事件。

    请注意,用来确定附加事件标识符字段的方法 RegisterRoutedEvent 实际上与用来注册非附加路由事件的方法相同。附加事件和路由事件都是在集中管理的内部存储区中进行注册的。此事件存储区实现则实现了路由事件概述中讨论的“事件作为界面”概念方面的注意事项。

    引发 WPF 附加事件
    通常不需要从代码中引发 WPF 定义的现有附加事件。这些事件采用一般的“服务”概念模型,而服务类(如 InputManager)则负责引发事件。

    但是,如果您根据将 RoutedEvent 作为附加事件基础的 WPF 模型来定义自定义附加事件,则可以使用 RaiseEvent 从任何 UIElement 或 ContentElement 中引发附加事件。引发路由事件(不管是否附加)要求您在元素树中声明一个特定的元素作为事件源;该事件源被报告为 RaiseEvent 调用方。您的服务负责决定将哪个元素报告为元素树中的事件源。

    1. WPF基础之布局系统

    本主题描述 Windows Presentation Foundation (WPF) 布局系统。了解在构造外观醒目、性能优良的用户界面时如何以及何时进行布局计算是非常重要的。

    布局系统
    术语“布局”描述测量和排列 Panel 元素的 Children 集合的成员、然后在屏幕上绘制它们的过程。这是一个计算密集型过程,即 Children 集合越大,执行的计算次数就越多。根据拥有该集合的 Panel 元素所定义的布局行为,还可能会增加复杂性。如果不需要较为复杂的 Panel(如 Grid),则可以使用构造相对简单的布局(如 Canvas),这种布局可产生更佳的性能。

    每当子 UIElement 改变其位置时,布局系统就可能触发一个新的处理过程。因此,了解哪些事件会调用布局系统就很重要,因为不必要的调用可能导致应用程序性能变差。

    简单地说,布局是一个递归系统,实现在屏幕上对元素进行大小调整、定位和绘制。布局系统为 Children 集合的每个成员完成两个处理过程:测量处理过程和排列处理过程。每个子 Panel 均提供自己的 MeasureOverride 和 ArrangeOverride 方法,以实现自己特定的布局行为。不论何时调用布局系统,都会发生以下系列事件。

    子 UIElement 通过首先测量它的核心属性来开始布局过程。

    计算在 FrameworkElement 上定义的大小调整属性,例如 Width、Height 和 Margin。

    应用 Panel 特定逻辑,例如 Dock 方向或堆栈 Orientation。

    测量所有子级后排列内容。

    Children 集合绘制到屏幕。

    如果其他 Children 添加到集合、应用 LayoutTransform 或调用 UpdateLayout 方法,会再次调用此过程。

    下面的小节将更详尽地定义此过程及其调用方式。

    元素边界框
    在 Windows Presentation Foundation (WPF) 中构思应用程序布局时,了解环绕所有元素的边界框非常重要。这有助于您理解布局系统的行为。布局系统使用的每个 FrameworkElement 可以被视为是嵌入到布局分区中的矩形。LayoutInformation 类会公开,可以返回元素布局分配的几何边界或槽。矩形的大小是由系统通过计算可用屏幕空间、任意约束的大小、布局特定属性(如边距和填充)及父 Panel 元素的个别行为来确定的。通过处理此数据,系统将能够计算给定的 Panel 的所有子级的位置。牢记在父元素上定义的哪些大小调整特性(如 Border)会影响其子级,这非常重要。

    例如,请考虑下面的简单布局方案。

    可以使用以下可扩展应用程序标记语言 (XAML) 来实现此布局。

    此单独的 TextBlock 元素是在 Grid 内承载的,而文本仅填充在其所在列的左上角,为 TextBlock 分配的空间实际更大。可以使用 GetLayoutSlot 方法检索任意 FrameworkElement 的边界框。使用此方法,TextBlock 元素的边界框是叠加的,这可能是因为 TextBlock 是在 Grid(它是允许共享布局坐标的 Panel 元素)内承载的。

    现在很明显该元素由白线环绕,分配给 TextBlock 元素的分区实际远远大于其填充的空间。由于还有其他元素添加到 Grid,此分配可能会收缩或扩展,这取决于所添加元素的类型和大小。

    将会使用 GetLayoutSlot 方法(它是一项用于显示元素边界框的有用技术)返回 TextBlock 的布局槽并将其转换成 Path。

    测量和排列子控件
    当呈现 Window 对象的内容时,会自动调用布局系统。为了显示内容,窗口的 Content 必须定义根 Panel(用于定义框架,Children 是按框架在屏幕上组织的)。

    布局的第一个处理过程是测量处理过程,将在此对 Children 集合的每个成员进行计算。此过程将以调用 Measure 方法开始。此方法将在父 Panel 元素的实现中调用,无需为要出现的布局显式调用该方法。

    首先,将计算 UIElement 的本机大小属性,如 Clip 和 Visibility。这将生成一个名为 constraintSize 的传递给 MeasureCore 的值。

    其次,会处理在 FrameworkElement 上定义的框架属性,这将影响 constraintSize 的值。这些属性旨在描述基础 UIElement 的大小调整特性,例如其 Height、Width、Margin 和 Style。上述每个属性均可能改变显示元素所必需的空间。然后,将用 constraintSize 作为一个参数调用 MeasureOverride。

    说明:
    在 Height、Width、ActualHeight 和 ActualWidth 的属性之间存在着差异。例如,ActualHeight 属性是基于其他高度输入和布局系统的计算值。该值是由布局系统本身基于实际呈现处理过程设置的,因此可能稍微小于属性(例如作为输入更改基础的 Height)的设置值。

    由于 ActualHeight 是一个计算值,所以您应该知道,作为布局系统多个操作的结果,该值可能有多次或不断增加的报告的更改。布局系统可能正在计算子元素所需的测量空间、父元素的约束等。

    测量处理过程的最终目标是让子级确定其 DesiredSize,这是在 MeasureCore 调用期间发生的。该值由 Measure 存储,以便在内容排列过程期间使用。

    此排列过程将以调用 Arrange 方法开始。在排列处理过程期间,父 Panel 元素生成一个代表子级边界的矩形。该值会传递给 ArrangeCore 方法以便进行处理。

    ArrangeCore 方法计算子级的 DesiredSize,计算可能影响该元素呈现大小的任何其他边距,并生成 arrangeSize(作为参数传递给 Panel 的 ArrangeOverride)。ArrangeOverride 生成子级的 finalSize,最后,ArrangeCore 方法执行偏移属性(例如边距和对齐方式)的最终计算,并将子级放在其布局槽内。子级无需(且通常不会)填充整个分配空间。然后,控件返回到父 Panel,至此布局过程完成。

     面板元素和自定义布局行为
    Windows Presentation Foundation (WPF) 包括 Panel 元素的派生套件,可以实现许多复杂的布局。常见方案(如堆栈元素)可以使用 StackPanel 元素方便地实现,而较为复杂和自由流动的布局可以使用 Canvas 来实现。

    下表概括了可用的布局元素。

    面板名称
    说明

    Canvas
    定义一个区域,在此区域内,您可以使用相对于 Canvas 区域的坐标显式定位子元素。

    DockPanel
    定义一个区域,在此区域中,您可以使子元素互相水平或垂直排列。

    Grid
    定义由行和列组成的灵活网格区域。

    StackPanel
    将子元素排列成一行(可沿水平或垂直方向)。

    VirtualizingPanel
    为“虚拟化”其子数据集合的 Panel 元素提供一个框架。这是一个抽象类。

    WrapPanel
    从左至右按顺序位置定位子元素,在包含框的边缘处将内容断开至下一行。后续排序按照从上至下或从右至左的顺序进行,具体取决于 Orientation 属性的值。
    对于其所需应用程序布局不可能使用任意预定义的 Panel 元素来实现的方案,您可以通过从 Panel 继承、并重写 MeasureOverride 和 ArrangeOverride 方法来实现自定义布局行为。

    布局性能注意事项
    布局是一个递归过程。Children 集合中的每个子元素会在每次调用系统期间得到处理。因此,应避免在不必要时触发系统。以下提示有助于实现更高的性能。

    其值可能导致布局系统被初始化的相关性属性会标记为公共标志。AffectsMeasure 和 AffectsArrange 提供有关哪个属性值更改会强制执行布局系统的递归更新的有用提示。通常,任何可能影响元素边界框大小的属性应将 AffectsMeasure 标志设置为 true。

    LayoutTransform 可能是影响用户界面 (UI) 内容的非常有用的方式。不过,如果转换的效果无需对其他元素的位置施加影响,则最好改为使用 RenderTransform,因为 RenderTransform 不会调用布局系统。LayoutTransform 会应用其转换,并强制对帐户执行递归布局更新,以获取受影响元素的新位置。

    避免不必要地调用 UpdateLayout。此方法强制递归布局更新,但常常却是不必要的。除非您确认需要进行完整更新,否则请依赖布局系统来为您调用此方法。

    当处理大型 Children 集合时,请考虑使用 VirtualizingStackPanel 而非常规 StackPanel。通过“虚拟化”子元素,VirtualizingStackPanel 仅在内存中保留当前位于父 ViewPort 内的对象。因此,在大多数情况下性能会得到极大改进。

    1. WPF基础之样式设置和模板化

    Windows Presentation Foundation (WPF) 样式设置和模板化是指一套功能(样式、模板、触发器和演示图板),应用程序、文档或用户界面 (UI) 的设计人员使用这些功能可以创建更好的视觉效果,也可以对其产品的统一外观进行标准化。尽管作者或设计人员可以对应用程序的外观逐个进行大量自定义操作,他们还是需要一个功能强大的样式设置和模板化模型,以便在应用程序内部和应用程序之间维护和共享外观。Windows Presentation Foundation (WPF) 就提供了这样的模型。

    WPF 样式设置模型的另一个功能是实现表示形式与逻辑的分离。这意味着,在开发人员使用 C# 或 Visual Basic 进行逻辑编程时,设计人员只需使用 XAML 即可设计程序的外观。

    本概述将讨论样式设置和模板化示例简介应用程序,该应用程序具有两个 TextBlock 元素,以及一个绑定到图像列表的 ListBox 控件:


    本概述主要讨论该应用程序的样式设置和模板化方面,而不讨论任何数据绑定概念。

    另外,了解资源也很重要,正是资源使得样式和模板得以重用。

    样式基础知识
    您可以将 Style 看作是将一组属性值应用到多个元素的捷径。例如,考虑下面的 TextBlock 元素及其默认外观:


    通过直接对每个 TextBlock 元素设置 FontSize 和 FontFamily 等属性可以更改默认外观。但是,如果希望 TextBlock 元素可以共享某些属性,则可以在 XAML 文件的 Resources 节中创建一个 Style,如下所示:

    将样式的 TargetType 设置为 TextBlock 类型时,该样式会应用于窗口中的所有 TextBlock 元素。

    现在,TextBlock 元素的外观如下:

    扩展样式
    您可能希望两个 TextBlock 元素共享某些属性值(如 FontFamily 和居中的 HorizontalAlignment),同时还希望文本“我的图片”具有某些其他属性。在第一个样式的基础上创建一个新样式可以达到这一目的,如下所示:

    请注意,已为上面的样式提供了 x:Key。若要应用该样式,请将 TextBlock 的 Style 属性设置为 x:Key 值,如下所示:

     现在,此 TextBlock 样式的 HorizontalAlignment 值为 Center,FontFamily 值为 Comic Sans MS,FontSize 值为 26,Foreground 值设置为示例所示的 LinearGradientBrush。请注意,我们已重写了基础样式的 FontSize 值。如果有多个 Setter 对 Style 的同一属性进行设置,则最后声明的 Setter 优先。

    下面演示 TextBlock 元素现在的外观:

    此 TitleText 样式扩展了为 TextBlock 类型创建的样式。此外,使用 x:Key 值也可以扩展具有 x:Key 的样式。

    TargetType 属性 (Property) 和 x:Key 属性 (Attribute) 的关系
    如第一个示例所示,如果将 TargetType 属性设置为 TextBlock 而不为样式分配 x:Key,样式就会应用于所有 TextBlock 元素。这种情况下,x:Key 隐式设置为 {x:Type TextBlock}。这意味着,如果将 x:Key 值显式设置为 {x:Type TextBlock} 之外的任何值,Style 就不会自动应用于所有 TextBlock 元素。此时,必须通过使用 x:Key 值,将样式显式应用于 TextBlock 元素。如果样式位于资源部分,并且未设置样式的 TargetType 属性,则必须提供 x:Key。

    除了提供 x:Key 的默认值之外,TargetType 属性还指定要应用 setter 属性的类型。如果未指定 TargetType,则必须通过语法 Property="ClassName.Property",用类名限定 Setter 对象的属性。例如,必须将 Property 设置为 "TextBlock.FontSize" 或 "Control.FontSize",而不要设置 Property="FontSize"。

    此外,还应注意,很多 WPF 控件都是由其他 WPF 控件组合而成的。如果创建要应用于某个类型的所有控件的样式,可能会得到意想不到的结果。例如,如果针对 Window 中的 TextBlock 类型创建一个样式,则该样式会应用于窗口中的所有 TextBlock 控件,即使 TextBlock 是另一个控件(如 ListBox)的组成部分也不例外。

    样式和资源
    任何派生自 FrameworkElement 或 FrameworkContentElement 的元素都可以使用样式。声明样式的最常见方式是将样式作为 XAML 文件的 Resources 节中的资源,如前面的示例所示。由于样式是资源,因此同样遵循所有资源都适用的范围规则:样式的声明位置决定样式的应用范围。例如,如果在应用程序定义 XAML 文件的根元素中声明样式,则样式可在应用程序范围内使用。如果创建的是导航应用程序,并在该应用程序的某个 XAML 文件中声明样式,则该样式只能在该 XAML 文件中使用。

    以编程方式设置样式
    若要以编程方式向元素分配命名样式,请从资源集合中获取该样式,然后将其分配给元素的 Style 属性。请注意,资源集合中的项是 Object 类型,因此,将检索到的样式分配给 Style 属性之前,必须将该样式强制转换为 Style。例如,若要对名为 textblock1 的 TextBlock 设置定义的 TitleText 样式,请执行以下操作:

    请注意,样式一旦应用,便会密封并且无法更改。如果要动态更改已应用的样式,必须创建一个新样式来替换现有样式。

    您可以创建一个根据自定义逻辑选择要应用的样式的对象。

    绑定、动态资源和事件处理程序
    请注意,使用 Setter.Value 属性可以指定 绑定标记扩展或 DynamicResource 标记扩展。

    到目前为止,我们只讨论了如何使用 setter 设置属性值。在样式中也可以指定事件处理程序。

    数据模板
    在本示例应用程序中,有一个绑定到照片列表的 ListBox 控件。

    此 ListBox 当前的外观如下所示:


    大多数控件都具有某种类型的内容,这些内容通常来自绑定到的数据。在本示例中,数据为照片列表。在 WPF 中,使用 DataTemplate 可以定义数据的可视表示形式。基本上,输入 DataTemplate 的内容决定了数据在呈现的应用程序中的外观。

    在我们的示例应用程序中,每个自定义 Photo 对象都具有一个字符串类型的 Source 属性,该属性指定图像的文件路径。当前,照片对象显示为文件路径。

    对于要显示为图像的照片,可以将 DataTemplate 作为资源创建:

    请注意,DataType 属性与 Style 的 TargetType 属性非常相似。如果 DataTemplate 位于资源部分,并且将 DataType 属性指定为某个类型,也不为其分配 x:Key,则只要该类型出现,便会应用 DataTemplate。任何时候都可以为 DataTemplate 分配 x:Key,然后将其设置为 DataTemplate 类型的属性(如 ItemTemplate 属性或 ContentTemplate 属性)的 StaticResource。

    实质上,上面示例的 DataTemplate 确定只要存在 Photo 对象,该对象就应作为 Image 显示在 Border 中。通过此 DataTemplate,应用程序现在的外观如下:


    数据模板化模型还提供其他功能。例如,如果要使用 HeaderedItemsControl 类型(如 Menu 或 TreeView)显示包含其他集合的集合数据,则可以使用 HierarchicalDataTemplate。另一个数据模板化功能是 DataTemplateSelector,利用这一功能可以根据自定义逻辑选择要使用的 DataTemplate。有关更多信息,请参见数据模板概述,该概述对不同的数据模板化功能进行了更加深入的讨论。

    控件模板

    请注意,我们的照片显示为图片,我们要水平显示这些照片,而不是垂直显示;我们希望 ListBox 是水平的。

    不使用 ControlTemplate
    首先,要使 ListBox 水平,不一定要使用 ControlTemplate,明确这一点很重要。ListBox 具有 ItemsPanel 属性,利用该属性可以设置 ItemsPanelTemplate,即控制 ListBox 的项的布局的模板。一种方法是只创建 ListBox 样式,然后设置 ItemsPanel 属性,如下例所示:

    例表明,除了替换 ControlTemplate 之外,可能还有其他方法,这取决于具体的情况。在本示例中,如果希望获得具有其他属性(如圆角)的水平 ListBox,则需要使用 ListBox 的 ControlTemplate。

    在提供示例来演示具体操作之前,首先需要说明 ControlTemplate 的概念。

    什么是 ControlTemplate?
    大多数控件都具有外观和行为。以按钮为例:外观是可以按下的凸起区域,行为是在响应单击时所引发的 Click 事件。

    有时,控件可以提供所需行为,但不具有所需外观。到目前为止,我们已经演示了可以使用样式 setter 来设置属性值,从而影响控件的外观。但是,若要更改控件的结构,或对组成控件的组件设置属性值,就需要使用 ControlTemplate。

    在 WPF 中,控件的 ControlTemplate 定义控件的外观。通过为控件定义新的 ControlTemplate 可以更改控件的结构和外观。很多情况下,这种方法都足够灵活,您不需要自己编写自定义控件。如果没有为控件定义自己的 ControlTemplate,则可以获取与系统主题匹配的默认模板,该模板向 Button 控件提供默认外观。

    请注意:一旦为控件创建 ControlTemplate,就会替换整个 ControlTemplate。例如,可以通过以下方式定义 Button ControlTemplate。

    请注意,ContentPresenter 元素只标记 Button 的 Content 应出现在何处。后面有一节专门展开详细讨论。

    应用此模板之后,Button 显示为 Ellipse:


    请记住,当 Button 具有焦点或按下时,其外观都是将替换的按钮的默认外观的组成部分。因此,您可能希望定义按钮按下时的外观,这取决于您的具体需要。

    如果要创建 ControlTemplate,使用 ControlTemplate 示例 是最好的入门方法。如果确实需要查看控件的组成部分,可以查看位于主题的主题文件,也可以使用 XAMLPad(随 Windows 软件开发工具包 (SDK) 安装的应用程序)的 Show Visual Tree 功能。

    创建 ControlTemplate
    现在,继续演示示例,我们创建一个 ControlTemplate,它定义一个水平的圆角 ListBox。若要替换控件的 ControlTemplate,请将 Template 属性设置为新的 ControlTemplate。

    以这种方式设置 Template 属性,实际上与使用 Style 设置其他控件属性没有区别:您将 Style 用作一个工具来帮助设置 Template 属性。也就是说,设置 ControlTemplate 的另一种方法是直接设置控件的 Template 属性。如果以这种方式设置,则会在 Resources 节中创建一个 ControlTemplate,并为它提供 x:Key,然后将它作为静态资源使用。

    如上例所示,ControlTemplate 类具有 TargetType 属性,该属性类似于 Style 类的 TargetType 属性。但要注意,与 Style 和 DataTemplate 不同,ControlTemplate 对象没有隐式键的概念。换言之,如果有一个独立 ControlTemplate,其 TargetType 属性设置为某个类型,则 ControlTemplate 不会自动应用于该类型。另请注意,如果模板定义包含 ContentPresenter,则 ControlTemplate 需要 TargetType 属性。

    尝试使用 ControlTemplate。例如,用 WrapPanel 替换 StackPanel,将 ScrollViewer 的 HorizontalScrollBarVisibility 属性设置为 Disabled,然后将 ListBox 的宽度设置为 300。(只有第一行空间不足时,WrapPanel 才会将项放置到下一行。如果没有将 ScrollViewer 的 HorizontalScrollBarVisibility 属性设置为 Disabled,由于可以滚动到末尾,则第一行不会空间不足。因此,WrapPanel 不会对项进行换行。)

    IsItemsHost 属性
    在此示例中,一个必需的重要属性是 IsItemsHost 属性。IsItemsHost 属性用于指示在 ItemsControl(如处理项列表的 ListBox 控件)的模板中,生成的元素应放在什么位置。如果将 StackPanel 的这一属性设置为 true,则添加到 ListBox 的所有项都将进入 StackPanel。请注意,此属性只对 Panel 类型有效。

    ItemsPresenter 和 ContentPresenter
    请注意,如果以这种方式在 ControlTemplate 中指定一个面板并将其标记为 IsItemsHost,控件的用户不使用 ControlTemplate 就无法替换 ItemsPanel。因此,除非您确信必须使用模板才能替换面板,否则不要采用这种方式。此外,您也可以使用 ItemsPresenter 元素来标记项的位置,然后通过设置 ItemsPanel 属性来指定 ItemsPanelTemplate。ItemsPanelTemplate 页提供了一个示例,为您演示如何操作。

    如果要创建 ContentControl(如 Button)的模板,则对应元素为 ContentPresenter。同样,将此元素放置到 ContentControl 类型的 ControlTemplate 中,可以指示内容应在什么位置显示,如什么是 ControlTemplate? 一节中的示例所示。有关其他示例,请参见 Label ControlTemplate 示例和 ListBoxItem ControlTemplate 示例。

    TemplateBinding
    在上一示例中,需要注意的另一个重点是设置为 {TemplateBinding ListBox.Background} 的 Background 值。它只是指示 Border 的 Background 应与 ListBox 上设置的 Background 值同步。TemplatBinding 与 Binding 类似。实际上,TemplatBinding 比 Binding 更有效,但功能更弱;使用 TemplatBinding 等效于使用 Source 属性设置为 RelativeSource.TemplatedParent 的 Binding。

    若要使控件用户能够控制某些属性的值,可以在 ControlTemplate 中使用 TemplateBinding。TemplateBinding 是一个由 TemplateBindingExtension 类表示的标记扩展。

    您可能已经注意到,DataTemplate 和 ControlTemplate 的相似之处在于它们的内容变成了对象的外观。通过 ListBox ControlTemplate 定义,应用程序现在的外观如下:


    触发器
    Style、ControlTemplate 和 DataTemplate 都具有 Triggers 属性,该熟悉可以包含一组触发器。某个属性值更改时,或某个事件引发时,触发器会相应地设置属性或启动操作(如动画操作)。

    属性触发器
    为了演示如何使用触发器来设置属性,我们将每个 ListBoxItem 都设置为部分透明(除非它被选中)。

    下面的样式将 ListBoxItem 的 Opacity 值设置为 0.5。但是,当 IsSelected 属性为 true 时,Opacity 设置为 1.0:

    此示例使用 Trigger 来设置属性值,但请注意,Trigger 类还具有 EnterActions 和 ExitActions 属性,通过这两个属性,触发器可以执行操作。

    请注意,我们还将 ListBoxItem 的 MaxHeight 属性设置为 75。在下面的屏幕快照中,选中的项是第三项:

    EventTrigger 和 Storyboard
    我们刚刚演示了 Trigger 根据某个属性的值来设置属性值或启动操作。另一种类型的触发器是 EventTrigger,它根据事件的引发来启动一组操作。例如,下面的 EventTrigger 对象指定当鼠标指针进入 ListBoxItem 时,MaxHeight 属性在 0.2 秒时间内以动画方式增大为值 90。当鼠标离开该项时,该属性在 1 秒时间内还原为原始值。请注意为何无需为 MouseLeave 动画指定 To 值。这是因为动画能够跟踪原始值。

    在下面的屏幕快照中,鼠标指向第三项:


    MultiTrigger、DataTrigger 和 MultiDataTrigger
    除了 Trigger 和 EventTrigger 之外,还有其他类型的触发器。通过 MultiTrigger,可以根据多个条件来设置属性值。如果条件的属性是经过数据绑定的,则可以使用 DataTrigger 和 MultiDataTrigger。

    共享的资源和主题
    典型 Windows Presentation Foundation (WPF) 应用程序可能具有多个在整个应用程序范围内应用的用户界面 (UI) 资源。概括地说,这组资源可视为应用程序的主题。通过使用封装为 ResourceDictionary 类的资源字典,Windows Presentation Foundation (WPF) 支持将用户界面 (UI) 资源打包为主题。

    Windows Presentation Foundation (WPF) 主题是使用样式设置和模板化机制定义的,Windows Presentation Foundation (WPF) 公开该机制,用于自定义任何元素的可视对象。

    Windows Presentation Foundation (WPF) 主题资源存储在嵌入式资源字典中。这些资源字典必须嵌入到已签名的程序集中,它们既可以嵌入到代码自身所在的程序集中,也可以嵌入到并行程序集中。对于包含 Windows Presentation Foundation (WPF) 控件的程序集 PresentationFramework.dll,主题资源在一系列并行程序集中。

    搜索元素样式时,主题是最后查找的位置。通常,搜索首先沿元素树向上查找相应资源,然后在应用程序资源集合中查找,最后查询系统。这为应用程序的作者提供了机会,让他们可以在到达主题之前,在树或应用程序级重新定义任何对象的样式。

    您可以将各资源字典分别定义为单个文件,这样就可以在多个应用程序中重用某个主题。通过定义多个提供相同类型资源、但具有不同值的资源字典,也可以创建可交换的主题。在设计应用程序外观时,建议在应用程序级重新定义这些样式或其他资源。

    若要在多个应用程序中共享一组资源(包括样式和模板),可以创建一个 XAML 文件并定义一个 ResourceDictionary。例如,请看下面的屏幕快照,它显示了使用 ControlTemplates 设置样式的示例的一部分:


    如果查看示例中的 XAML 文件,您会注意到所有文件都包含以下内容:

    这是共享的 shared.xaml,该文件定义一个 ResourceDictionary,该资源字典包含一组样式和画笔资源,使示例中的控件具有了一致的外观。

    1. 详谈WPF开发中的数据虚拟化

    UI虚拟化

    当一个WPF的ItemControl被绑定到一个大型集合的数据源时,如果可以UI虚拟化,该控件将只为那些在可以看到的项创见可视化的容器(加上面和下面的少许)。这是一个完整集合中有代表性的一小部分。用户移动滚动条时,将为那些滚动到可视区域的项创建新的可视化容器,那些不再可见的项的容器将被销毁。当容器设置为循环使用时,它将再使用可视化容器代替不断的创建和销毁可视化容器,避免对象的实例化和垃圾回收器的过度工作。

    数据虚拟化

    数据虚拟化是指绑定到ItemControl的真实的数据对象的归档虚拟化的时间段。数据虚拟化不是由WPF提供的。作为对比,基本数据对象的小集合对内存的消耗不是很多;但是,大集合的内存消耗是非常严重的。另外,真实的检索数据(例如,从数据库)和实例化数据对象是很耗时的,尤其当是网络数据调用时。因此,我们希望使用数据虚拟化机制来限制检索的数据的数量和在内存中生成数据对象的数量。

    解决方案

    总览

    这个解决方案是只在ItemControl绑定到IList接口的实现时起作用,而不是IEumerable的实现,它并不枚举整个列表,而只是读取需要显示的项。它使用Count属性判断集合的大小,推测并设置滚动的范围。然后使用列表索引重新确定要在屏幕上显示的项。因此,创建一个可以报告具有大量项的,并且可以只检索需要的项的IList。

    IItemsProvider 为了利用这个解决方案,下面的数据源必须能提供集合中项的数量,并且能够提供完整集合的小块(或页)。这需要在IItemsProvider接口封装。

    ///
    /// Represents a provider of collection details.
    ///
    /// The type of items in the collection.
    public interface IItemsProvider
    {
    ///
    /// Fetches the total number of items available.
    ///
    ///
    int FetchCount();
    ///
    /// Fetches a range of items.
    ///
    /// The start index.
    /// The number of items to fetch.
    ///
    IList FetchRange(int startIndex, int count);
    }

    如果下面的查询是一个数据库查询,它是一个利用大多数据库供应商都提供的COUNT()聚集函数和OFFSET与LIMIT表达式的一个IItemProvider接口的一个简单实现。

    VirtualizingCollection 这是一个执行数据虚拟化的IList的实现。VirtualizingCollection(T)把整个集合分装到一定数量的页中。根据需要把页加载到内存中,在不需要时从释放。

    下面讨论我们有兴趣的部分。详细信息请参考附件中的源代码项目。

    IList实现的第一个方面是实现Count属性。它通常被ItemsControl用来确定集合的大小,并呈现适当的滚动条。

    private int _count = -1;
    public virtual int Count
    {
    get
    {
    if (_count == -1)
    {
    LoadCount();
    }
    return _count;
    }
    protected set
    {
    _count = value;
    }
    }
    protected virtual void LoadCount()
    {
    Count = FetchCount();
    }
    protected int FetchCount()
    {
    return ItemsProvider.FetchCount();
    }

    Count属性使用延迟和懒惰加载(lazy loading)模式。它使用特殊值-1作为未加载的标识。当第一次读取它时,它从ItemsProvider加载其实际的数量。

    IList接口的实现的另一个重要方面是索引的实现。

    private int _count = -1;
    public virtual int Count
    {
    get
    {
    if (_count == -1)
    {
    LoadCount();
    }
    return _count;
    }
    protected set
    {
    _count = value;
    }
    }
    protected virtual void LoadCount()
    {
    Count = FetchCount();
    }
    protected int FetchCount()
    {
    return ItemsProvider.FetchCount();
    }

    这个索引是这个解决方案的一个聪明的操作。首先,它必须确定请求的项在哪个页(pageIndex)中,在页中的位置(pageOffset),然后调用RequestPage()方法请求该页。

    附加的步骤是然后根据pageOffset加载后一页或前一页。这基于一个假设,如果用户正在浏览第0页,那么他们有很高的机率接下来要滚动浏览第1页。提前把数据取来,就可以无延迟的显示。

    然后调用CleanUpPages()清除(或卸载)所有不再使用的页。

    最后,放置页不可用的一个防御性的检查, 当RequestPage没有同步操作时是必要的,例如在子类AsyncVirtualizingCollection中。

    // ...
    private readonly Dictionary> _pages =
    new Dictionary>();
    private readonly Dictionary _pageTouchTimes =
    new Dictionary();
    protected virtual void RequestPage(int pageIndex)
    {
    if (!_pages.ContainsKey(pageIndex))
    {
    _pages.Add(pageIndex, null);
    _pageTouchTimes.Add(pageIndex, DateTime.Now);
    LoadPage(pageIndex);
    }
    else
    {
    _pageTouchTimes[pageIndex] = DateTime.Now;
    }
    }
    protected virtual void PopulatePage(int pageIndex, IList page)
    {
    if (_pages.ContainsKey(pageIndex))
    _pages[pageIndex] = page;
    }
    public void CleanUpPages()
    {
    List keys = new List(_pageTouchTimes.Keys);
    foreach (int key in keys)
    {
    // page 0 is a special case, since the WPF ItemsControl
    // accesses the first item frequently
    if ( key != 0 && (DateTime.Now -
    _pageTouchTimes[key]).TotalMilliseconds > PageTimeout )
    {
    _pages.Remove(key);
    _pageTouchTimes.Remove(key);
    }
    }
    }

    页存储在以页索引为键的字典(Dictionary)中。一个附加的字典(Dictionary)记录着每个页的最后存取时间,它用于在CleanUpPages()方法中移除较长时间没有存取的页。

    protected virtual void LoadPage(int pageIndex)
    {
    PopulatePage(pageIndex, FetchPage(pageIndex));
    }
    protected IList FetchPage(int pageIndex)
    {
    return

    为完成该解决方案,FetchPage()执行从ItemProvider中抓取数据,LoadPage()方法完成调用PopulatePage方法获取页并把该页存储到字典(Dictionary)中的工作。

    看起来好象有一些太多的不全逻辑的方法(a few too many inconsequential methods),但这样设计是有原因的:每一个方法做且只做一件事,有助于提高代码的可读性,并使在子类中进行功能扩展和维护变得容易,下面可以看到。

    类VirtualizingCollection实现了数据虚拟化的基本目标。不幸的是,在使用中,它有一个严重不足:数据抓取方法是全部同步执行的。这就是说它们要在UI线程中执行,造成一个缓慢的程序

    AsyncVirtualizingCollection类AsyncVirtualizingCollection继承自VirtualizingCollection,重载了Load方法,以实现数据的异步加载。

    WPF中异步数据源的关键是在数据抓取完成后必须通知UI的数据绑定。在规则的对象中,是通过实现INotifyPropertyChanged接口实现的。对一个集合的实现,需要紧密的关系,INotifyCollectionChanged。那是ObservableCollection要使用的接口。

    public event NotifyCollectionChangedEventHandler CollectionChanged;
    protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
    NotifyCollectionChangedEventHandler h = CollectionChanged;
    if (h != null)
    h(this, e);
    }
    private void FireCollectionReset()
    {
    NotifyCollectionChangedEventArgs e =
    new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
    OnCollectionChanged(e);
    }
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
    {
    PropertyChangedEventHandler h = PropertyChanged;
    if (h != null)
    h(this, e);
    }
    private void FirePropertyChanged(string propertyName)
    {
    PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);
    OnPropertyChanged(e);
    }

    实现了INotifyCollectionChanged接口和INotifyPropertyChanged接口。提供数据绑定弹性最大化。这个实现没有任何要注意的。

    protected override void LoadCount()
    {
    Count = 0;
    IsLoading = true;
    ThreadPool.QueueUserWorkItem(LoadCountWork);
    }
    private void LoadCountWork(object args)
    {
    int count = FetchCount();
    SynchronizationContext.Send(LoadCountCompleted, count);
    }
    private void LoadCountCompleted(object args)
    {
    Count = (int)args;
    IsLoading = false;
    FireCollectionReset();
    }

    在重载的LoadCount()方法中,抓取是由ThreadPool(线程池)异步调用的。一旦完成,就会重置Count,UI的更新是由INotifyCollectionChanged接口调用FireCollectionReset方法实现的。注意LoadCountCompleted方法会在UI线程通过SynchronizationContext再一次被调用。假定集合的实例在UI线程中被创建,SynchronationContext属性就会被设置。

    protected override void LoadPage(int index){IsLoading = true;

    ThreadPool.QueueUserWorkItem(LoadPageWork, index);}

    private void LoadPageWork(object args){   

    int pageIndex = (int)args;    IList page = FetchPage(pageIndex);

    SynchronizationContext.Send(LoadPageCompleted, new object[]{pageIndex, page});}

    private void LoadPageCompleted(object args){int pageIndex=(int)((object[]) args)[0];

    IList page = (IList)((object[])args)[1];    PopulatePage(pageIndex, page);

    IsLoading = false;    FireCollectionReset();}

    页数据的加载遵循相同的惯例,再一次调用FireCollectionReset方法更新用户UI。

    也要注意IsLoading属性是一个简单的标识,可以用来告知UI集合正在加载。当IsLoading改变后,由INotifyPropertyChanged机制调用FirePropertyChanged方法更新UI。

    public bool IsLoading{ get{ return _isLoading; }   

    set {if ( value != _isLoading ){  _isLoading = value;

    FirePropertyChanged("IsLoading");}    }}

    演示项目

    为了演示这个解决方案,我创建了一个简单的示例项目(包括附加的源代码项目)。

    首先,创建一个IItemsProvider的一个实现,它通过使用线程休眠来模拟网络或磁盘行为的延迟提供虚拟数据。

    public class DemoCustomerProvider : IItemsProvider
    {
    private readonly int _count;
    private readonly int _fetchDelay;
    public DemoCustomerProvider(int count, int fetchDelay)
    {
    _count = count;
    _fetchDelay = fetchDelay;
    }
    public int FetchCount()
    {
    Thread.Sleep(_fetchDelay);
    return _count;
    }
    public IList FetchRange(int startIndex, int count)
    {
    Thread.Sleep(_fetchDelay);
    List list = new List();
    for( int i=startIndex; i
            {
    Customer customer = new Customer {Id = i+1, Name = "Customer " + (i+1)};
    list.Add(customer);
    }
    return list;
    }
    }

    普遍存在的Customer(消费者)对象作为集合中的项。

    为了允许用户试验不同的列表实现,创建一个包含ListView的简单WPF窗体。

        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    
    Title="Data Virtualization Demo - By Paul McClean" Height="600" Width="600">
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
                      TextAlignment="Right" VerticalAlignment="Center"/>
    
    
    
                      Text="1000000" Width="60" VerticalAlignment="Center"/>
    
    
    
                      TextAlignment="Right" VerticalAlignment="Center"/>
    
    
    
                      Text="1000" Width="60" VerticalAlignment="Center"/>
    
    
    
    
    
    
    
    
    
    
    
    
    
                          TextAlignment="Right" VerticalAlignment="Center"/>
    
    
    
                          Margin="5" Content="List(T)" VerticalAlignment="Center"/>
    
    
    
                          Margin="5" Content="VirtualizingList(T)" 
    
    VerticalAlignment="Center"/>
    
    
    
                          Margin="5" Content="AsyncVirtualizingList(T)" 
    
    IsChecked="True" VerticalAlignment="Center"/>
    
    
    
    
    
    
    
                          TextAlignment="Right" VerticalAlignment="Center"/>
    
    
    
                          Text="100" Width="60" VerticalAlignment="Center"/>
    
    
    
                          TextAlignment="Right" VerticalAlignment="Center"/>
    
    
    
                          Text="30" Width="60" VerticalAlignment="Center"/>
    
    
    
    
    
    
    
    
    
    
    
                  VerticalAlignment="Center"/>
    
    
    
                  Width="80" VerticalAlignment="Center"/>

     

     

    XAML语法

    1. XAML语法术语

    本主题定义用来描述可扩展应用程序标记语言 (XAML) 语法的各个元素的术语。这些术语将在本软件开发工具包 (SDK) 的其余部分中经常用到。本主题扩展了 XAML 概述中所介绍的基本术语。

    XAML 语法术语的起源
    此处定义的 XAML 语法术语在 XAML 语言规范中也有定义或引用。XAML 是一种基于 XML 且遵循 XML 结构规则的语言。其中的术语共享自或基于描述 XML 语言或 XML 文档对象模型 (DOM) 时的常用术语。

     对象元素语法
    对象元素语法是一种 XAML 标记语法,它通过声明 XML 元素来实例化公共语言运行库 (CLR) 类或结构。此语法与其他标记语言(例如 HTML)的元素语法相似。对象元素语法以左尖括号 (<) 开头,其后紧跟正进行实例化的类或结构的类型名称。类型名称后面可以有零个或多个空格,对于对象元素还可以声明零个或多个属性,并用一个或多个空格来分隔每个“属性名="值"”对。最后,必须存在下列一种情况:

    元素和标记必须用正斜杠 (/) 和紧跟的右尖括号 (>) 结尾。

    开始标记必须以右尖括号 (>) 结尾。其他对象元素、属性元素或内部文本可以跟在开始标记后面。对象元素还必须存在等效的结束标记,并与其他开始标记/结束标记对形成正确的嵌套和平衡。

    例如,下面的示例是一个对象元素语法,该语法实例化 Button 类的一个新实例,而且还指定了一个 Name 属性及其值:

    下面的示例是一个还包括可扩展应用程序标记语言 (XAML) 内容属性语法的对象元素语法。其中包含的内部文本将用来设置 TextBox 可扩展应用程序标记语言 (XAML) 内容属性 Text。

     

    属性语法
    属性语法是一种 XAML 标记语法,该语法通过针对元素声明属性 (Attribute) 来设置属性 (Property) 值或者命名事件的事件处理程序。元素总是通过对象元素语法来声明。属性名必须与属性或事件的 CLR 成员名称相匹配。属性名后面是赋值运算符 (=)。属性值必须是一个用双引号 (") 引起来的字符串。

    为了能够通过属性语法进行设置,属性必须是公共的、可读写的,而且必须具有一个可以由 XAML 处理器实例化或引用的属性值类型。对于事件来说,事件必须是公共的而且必须具有一个公共委托。属性或事件必须是由包含对象元素实例化的类或结构的成员。

    属性值由下面的操作之一,按照如下处理顺序进行填充:

    如果 XAML 处理器遇到一个大括号,或者遇到一个从 MarkupExtension 派生的对象元素,则将首先计算所引用的标记扩展(而不是将该扩展作为字符串来处理),而且将使用由标记扩展返回的对象。在许多情况下,由标记扩展返回的对象将是对现有对象的引用,或者是一个将计算推迟到运行时的表达式,而不是一个新对象。

    如果该属性 (Property) 是用指定的 TypeConverter 声明的,或者该属性 (Property) 的值类型是用属性 (Attribute) 化 TypeConverter 声明的,则该属性 (Attribute) 的字符串值将作为转换输入提交到类型转换器,该转换器将返回一个新的对象实例。

    如果没有 TypeConverter,则将尝试直接转换为属性类型。最后一个级别是直接在基元类型之间转换,或者在枚举中检查名称(这将返回匹配的值)。

    例如,在使用上面所显示的标记时,可以使用下面的属性 (Attribute) 语法示例为 Name 属性 (Property) 赋予字符串值:

    Name 属性是 Button 类的成员表的成员。Button 是用来定义 Name 的 FrameworkElement 类的派生类。

    属性值的处理
    包含在左引号和右引号之间的字符串值是由 XAML 处理器处理的。对于属性来说,默认处理行为是由基础 CLR 属性的类型确定的。如果该属性 (Property) 是基元类型,则会基于字符串到相关基元类型的隐式转换来赋予属性 (Attribute) 值。如果该属性是一个枚举,则字符串会被视为由该枚举定义的名称,而且将从枚举中返回匹配的值。如果该属性 (Property) 既不是基元类型又不是枚举,则属性 (Attribute) 值必须由针对该属性 (Property) 本身或者目标类型声明的类型转换器来处理。类型转换器必须提供一个能够接受字符串的转换机制,该转换机制必须生成基础 CLR 属性类型的实例。还可以通过标记扩展来推迟转换步骤。

    枚举属性值
    XAML 中的枚举值由 Enum 结构的本机方法在内部处理。

    对于无标志的枚举值,本机行为是处理属性值的字符串并将它解析为某个枚举值。您不必像在代码中那样指定格式为枚举.值 的枚举,而是仅指定值,枚举 将从所设置属性的类型推断。如果您指定格式为枚举.值 的属性,它将无法正确解析。

    对于按标志枚举,该行为基于 Enum..::.Parse 方法。您可以通过用逗号分隔每个值来为按标志枚举指定多个值。但是,您不能合并不按标志的枚举值。例如,不能试图使用逗号语法来创建作用于无标志枚举多个条件的 Trigger:

    在 WPF 中,能够支持 XAML 中可设置属性的按标志枚举极为罕见。但是,StyleSimulations 就是这样的一个枚举。例如,可以使用逗号分隔的按标志属性语法来修改在 Glyphs 类的“Remarks”(备注)部分中提供的示例;StyleSimulations = "BoldSimulation" 可能会变成 StyleSimulations = "BoldSimulation,ItalicSimulation"。KeyBinding..::.Modifiers 是另一个属性,在该属性中可以指定多个枚举值。但是,此属性是一个特例,因为 ModifierKeys 枚举支持其自身的类型转换器。修饰符的类型转换器使用加号 (+) 而不是逗号 (,) 作为分隔符,因此在标记中支持用更传统的语法来表示组合键(如“Ctrl+Alt”)。

    属性引用和事件成员名称引用
    在指定属性 (Attribute) 时,可以引用您已经为包含对象元素实例化的 CLR 类型的成员表中存在的任何属性 (Property) 或事件。

    或者,可以独立于包含对象元素来引用附加属性或附加事件。

    对于可通过默认命名空间访问的任何对象中的任何事件,还可以通过使用类型名.事件 部分限定名来命名。此语法支持为路由事件附加处理程序,在路由事件中,处理程序旨在处理子元素中的事件路由,但是父元素在其成员表中并不拥有该事件。此语法与附加事件语法相似,但此处的事件不是真正的附加事件。相反,您引用的是具有限定名称的事件。

    属性 (Property) 名有时作为属性 (Attribute) 值提供,而不是作为属性 (Attribute) 名提供,属性 (Property) 名还可以包括限定符,例如以所有者类型.依赖项属性名称 格式指定的属性。在 XAML 中编写样式或模板时,此情况较为常见。以属性 (Attribute) 值形式提供的属性 (Property) 名具有不同的处理规则,这些规则由所设置的属性 (Property) 类型以及某些上下文因素(如样式或模板是否具有目标类型)来控制。

    当属性 (Attribute) 值描述属性 (Property) 之间的关系时,也可以使用属性 (Property) 名。此功能可用于数据绑定和演示图板目标,而且由 PropertyPath 类及其类型转换器启用。

    隐式集合元素会在逻辑树中创建一个成员,即使它在标记中不显示为元素也是如此。通常,所拥有类型的构造函数针对作为其属性之一的集合执行实例化,这会将该集合添加到树中。

    说明:
    由 WPF XAML 处理器执行的集合检测功能不支持泛型列表和字典接口(IList<(Of <(T>)>) 和 IDictionary<(Of <(TKey, TValue>)>))。但是,可以将 List<(Of <(T>)>) 类用作基类(因为它直接实现 IList),或者将 Dictionary<(Of <(TKey, TValue>)>) 用作基类(因为它直接实现 IDictionary)。

     XAML 内容语法
    XAML 内容语法仅在将 ContentPropertyAttribute 指定为其类声明一部分的类上启用。ContentPropertyAttribute 需要一个按名称指定属性的参数,而该属性名称被定义为这种类型元素(包括派生的类)的内容属性。为此指定的属性是元素的 XAML 内容属性。在由 XAML 处理器处理时,在元素的开始标记和结束标记之间找到的任何子元素或内部文本将被指定为该 XAML 内容属性的值。元素的属性元素标记不按照这种方式赋值;它们是先进行处理,而且不被视为“内容”。

    正如对于任何其他属性一样,对象的 XAML 内容属性将属于特定类型。该类型可以是 Object 类型。该内容属性的类型可帮助定义对象的内容模型。例如,鉴于任何对象都可以变为内容,Object 的类型是松散的,但是即使这种松散类型也要求内容必须是单个对象。该单个对象可以是集合对象,但是即便如此,也只能将一个这样的集合对象指定为内容。

    特定类型的内容模型在该类型的类页面上进行描述,或者编写成类型系列的单独概念性主题中并与每个相关的类型引用建立链接。

    集合类型的内容语法
    为了接受多个对象元素(或内部文本)作为内容,内容属性的类型必须是明确的集合类型。与集合类型的属性元素语法相似,XAML 处理器必须标识作为集合类型的类型。如果某个元素具有 XAML 内容属性,则该 XAML 内容属性的类型是集合。不必在标记中将隐含集合类型指定为对象元素,也不必将 XAML 内容属性指定为属性元素。因此,标记中明显的内容模型现在可以将多个子元素作为指定为内容。下面是 Panel 子类的内容语法。所有的 Panel 派生类都建立要成为 Children 的 XAML 内容属性,这需要一个类型为 UIElementCollection 的值。

    请注意,标记中既不需要 Children 的属性元素也不需要 UIElementCollection 的元素。这是 XAML 的设计特征,其目的在于,使用直接的父-子元素关系将那些用来定义 UI 的递归包含的元素更直观地表示为嵌套元素树,而不必对属性元素标记或集合对象进行外部干预。实际上,按照设计,UIElementCollection 在标记中不能指定为对象元素。由于 UIElementCollection 唯一的用途就是作为隐式集合,因此它不公开公共的默认构造函数,因此不能实例化为对象元素。

    在具有内容属性的对象中混合使用属性元素和对象元素
    XAML 规范声明 XAML 处理器可以进行如下强制:用来填充某个对象元素中 XAML 内容属性的对象元素必须是连续的,而且不得混合使用。对于混合使用属性元素和内容的这一限制是由 WPF XAML 处理器强制的。

    可以将子对象元素作为某个对象元素中的第一个直接标记,然后可以引入属性元素。也可以指定一个或多个属性元素,接着指定内容,然后指定多个属性元素。但是,一旦内容后面跟有属性元素,您就不能进一步引入任何内容,而只能引入其他属性元素。

    这个内容/属性元素顺序要求不适用于用作内容的内部文本。然而,这仍然是使内部文本保持连续的不错的标记样式,原因是,如果属性元素与内部文本交错分布,则很难直观地检测标记中的大量空白。

     附加属性
    附加属性是 XAML 中引入的一个编程概念,借此,属性可以由类型拥有和定义,但可以在任何元素上设置。附加属性所面向的主要方案就是,允许元素树中的子元素向父元素报告信息,而不要求使用在所有的元素之间广泛共享的对象模型。相反,附加属性可以由任何父元素用来向子元素报告信息。

    附加属性使用的语法在表面上与属性元素语法非常相似,因为您还需要指定类型名.属性名 组合。二者有两个重要的差异:

    即使在通过属性语法设置附加属性时,也可以使用类型名.属性名 组合。只有附加属性 (Property) 才要求属性 (Attribute) 语法中使用限定属性 (Property) 名。

    对于附加属性还可以使用属性元素语法。但是,对于典型的属性元素语法,您指定的类型名 是包含属性元素的对象元素。如果您引用的是附加属性,则类型名 是用来定义附加属性的类,而不是包含对象元素。

     附加事件
    附加事件是 XAML 中引入的另一个编程概念,事件可以由类型定义,但是处理程序可以附加到任何对象上。用来定义附加事件的类型通常是用来定义服务的静态类型,这些附加事件有时由用来公开服务的类型中的路由事件别名公开。附加事件的处理程序是通过属性语法指定的。正如对于附加事件一样,可以扩展附加事件的属性语法,以便允许使用类型名.事件名,其中类型名 是为附加事件基础结构提供 Add 和 Remove 事件处理程序访问器的类,事件名 是事件名称。

     XML 命名空间
    上面的所有语法示例均未指定默认命名空间以外的命名空间。在典型的 WPF 应用程序中,默认命名空间被指定为 WPF 命名空间。您可以指定默认命名空间以外的命名空间,而且仍使用实质上同类的语法,但是,只要命名了无法在默认命名空间中访问的类,该类的名称就必须以用来映射对应 CLR 命名空间的 XML 命名空间的前缀作为开头。例如, 是一种用来实例化 MyElement 类的实例的对象元素语法,其中包含该类的 CLR 命名空间(可能还有包含该命名空间的外部程序集)以前映射到 custom 前缀。

    标记扩展
    XAML 定义了一个标记扩展编程实体,该实体允许从 XAML 处理器对属性或对象元素的常规处理中进行转义,将该处理转给支持类。WPF 对 XAML 处理器的实现将 MarkupExtension 抽象类用作由 WPF 支持的所有标记扩展的基础。在使用属性语法时,用来标识 XAML 处理器的标记扩展的字符是左大括号 ({),其后是右大括号 (}) 以外的任何字符。左大括号后面的第一个字符串必须引用用来提供特定扩展行为的类,如果子字符串“Extension”是实际类名的一部分,则该引用可以省略这个子字符串。该类后面可能会出现一个空格,该空格后面的每个字符都可以由所实现的扩展用作输入,直到遇到右大括号。在使用属性语法时,WPF 中标记扩展的主要用途是提供一种方法来引用其他已经存在的对象,或者将引用转给将在运行时计算的对象。例如,可以指定用 {Binding} 标记扩展来代替给定的属性通常将使用的值类型,从而完成简单的数据绑定。对于无法以其他方式使用属性 (Attribute) 语法的属性 (Property),许多标记扩展都允许使用属性 (Attribute) 语法。例如,Style 对象是一个相对复杂的引用类型,其中包含几个其他属性,每个属性都还采用 byref 对象(而非基元)。但是样式通常作为资源来创建,之后将通过请求资源的两个标记扩展之一来引用。该扩展将对属性 (Property) 值的计算推迟到资源查找时,允许在属性 (Attribute) 语法中提供 Style 属性 (Property) 的值并采用 Style 类型,如下所示:

    My button 

    在这里,StaticResource 用来标识 StaticResourceExtension 类,该类提供标记扩展实现。下一个字符串 MyStyle 用作非默认 StaticResourceExtension 构造函数的输入,在该构造函数中,从扩展字符串提取的参数用来声明所请求的 ResourceKey。MyStyle 应当是定义为资源的 Style 的 x:Key 属性 值。StaticResource 标记扩展用法要求使用该资源,在加载时通过静态资源查找逻辑来提供 Style 属性值。

    可选的和不建议的 XAML 用法
    属性元素的可选用法
    属性元素的可选用法包括,具体地“拼出”由 XAML 处理器视为隐式的元素内容属性。例如,当您声明 Menu 的内容时,可以选择将 Menu 的 Items 集合显式声明为 属性元素标记,并将每个 MenuItem 放在 中,而不是使用隐式的 XAML 处理器行为(即,Menu 的所有子元素都必须是 MenuItem 而且放在 Items 集合中)。有时,这个可选用法可以帮助以可视方式阐明标记中所表示的对象结构。或者,属性元素的隐式用法有时可以避免使用在技术上具有功能但是在视觉上容易引起混淆(如在属性值中嵌套标记扩展)的标记。

    typeName.memberName 全限定属性
    使用属性的类型名.成员名 格式实际上比仅仅使用路由事件的情况更为普遍,但是,在其他应用程序中,如果只是为了实现标记样式和可读性,则该格式是多余的,您应当避免使用它。在下面的示例中,对 Background 属性的三个引用是完全等效的:

    Button.Background 之所以适用,是因为在 Button 上对于该属性的查找是成功的(Background 是从 Control 继承的),而且 Button 是对象元素的类或者是基类。Control.Background 之所以适用,是因为 Control 类实际上定义 Background,而且 Control 是一个 Button 基类。

    但是,下面的类型名.成员名 格式示例并不适用,因此显示为已注释掉:

    Label 是 Control 的另一个派生类,而且,如果在 Label 对象元素中指定了 Label.Background,则该用法将适用。但是,由于 Label 不是 Button 的类或基类,因此指定的 XAML 处理器行为是随后以附加属性形式处理 Label.Background。Label.Background 不是附加属性,因此该用法将失败。

    baseTypeName.memberName 属性元素
    与类型名.成员名 格式如何适用于属性语法相似,基类型名称.成员名 语法适用于属性元素语法。例如,下面的语法适用:

    在这里,即使属性元素包含在 Button 中,属性元素也会以 Control.Background 形式提供。

    但是,正如属性的类型名.成员名 格式一样,基类型名称.成员名 在标记中是很差的样式,您应当避免将其用于设置样式。

    1. 代码隐藏和XAML

    代码隐藏是一个术语,用于描述与将 XAML 页编译为应用程序时由 XAML 处理器创建的代码联接的代码。本主题描述代码隐藏的要求以及在 XAML 中的代码的可选内联代码机制。

    先决条件
    本主题假设您已阅读 XAML 概述并已基本了解 CLR 和面向对象的编程。

    代码隐藏、事件处理程序和分部类要求
    分部类必须派生自用作根元素的类的类型。您可以在代码隐藏的分部类定义中将派生留空,但编译的结果会假定页根作为分部类的基类,即使在没有指定的情况下也是如此(因为分部类的标记部分确实将页根指定为基)。

    编写的事件处理程序必须是 x:Class 标识的命名空间中的分部类所定义的实例方法。您不能限定事件处理程序的名称来指示 XAML 处理器在其他类范围中查找该处理程序,也不能将静态方法用作事件处理程序。

    事件处理程序必须与相应事件的委托匹配。

    专门针对 Microsoft Visual Basic .NET 语言,您可以使用特定于语言的 Handles 关键字将处理程序与处理程序声明中的实例和事件关联,而不是在 XAML 中将处理程序附加到属性。但是,这一技术确实存在一些限制,因为 Handles 不支持 WPF 事件系统的所有特定功能,例如某些路由事件方案或附加事件。

    x:Code
    x:Code 是在 XAML 中定义的一种指令元素。x:Code 指令元素可以包含内联编程代码。内联定义的代码可以与同一页中的 XAML 进行交互。下面的示例阐释了内联 C# 代码。请注意,该代码位于 x:Code 元素内,并且必须包围在 内,以便针对 XML 对内容进行转义,这样 XAML 处理器(解释 XAML 架构或 WPF 架构时)不会试图按原义将内容解释为 XML。

     

    内联代码限制
    应注意对基于 XAML 的应用程序避免或限制使用内联代码。在体系结构和编码原理方面,保留标记和代码隐藏之间的独立性可以更显著地区分设计人员和开发人员这两个角色。从更为技术性的角度看,为内联代码编写的代码更难编写,因为您总是要写入 XAML 页的生成的分部类中,并且只能使用默认的命名空间映射。因为不能添加 using 语句,因此必须完全限定您所进行的大量 API 调用。默认的 WPF 映射包括在 WPF 程序集中出现的大多数但并非全部的 CLR 命名空间;您必须完全限定对其他命名空间中包含的 API 的调用。此外,您还不能在内联代码中定义多个类,并且所有代码实体必须作为生成的分部类中的一个成员或变量存在。其他特定于语言的编程功能(例如宏或对全局变量或生成变量的 #ifdef)也不可用。

    1. XAML和自定义类

    可扩展应用程序标记语言 (XAML) 支持使用任何公共语言运行库 (CLR) 语言定义自定义类或结构,然后使用 XAML 标记(包括在同一标记文件中混合使用 Windows Presentation Foundation (WPF) 定义的 XAML 和自定义类的 XAML 标记)访问该类的功能。本主题讨论自定义类要用作 XAML 元素时所必须满足的要求。

    应用程序或程序集中的自定义类
    可以使用两种不同的方法定义 XAML 中使用的自定义类:在生成主 Windows Presentation Foundation (WPF) 应用程序的代码隐藏或其他代码中定义,或者在单独的程序集(如用作类库的可执行文件或 DLL)中定义为类。这些方法中的每一种都有特定的优点和缺点。

    创建类库的优点是,任何这样的自定义类都可以在许多可能不同的应用程序中共享。单独的类库也使应用程序的版本问题更易控制,而且也简化了在 XAML 页上创建要用作根元素的类这一过程。

    在应用程序中定义自定义类的优点是,此方法是相对轻量的方法,可最大限度减少当引入主可执行文件之外的单独程序集时遇到的部署和测试问题。但是,一个显著的缺点是,不能将同一程序集中定义的类用作 XAML 页的根元素。

    无论是在相同还是不同程序集中定义自定义类,都需要在 CLR 命名空间和 XML 命名空间之间映射这些自定义类才能在 XAML 中使用它们。

    自定义类作为 XAML 元素的要求
    类要能够实例化为对象元素,必须满足以下要求:

    自定义类必须是公共的且支持默认(无参数)公共构造函数。(托管代码结构隐式支持这样的构造函数。)

    自定义类不能是嵌套类(嵌套类和其语法中的“点”会干扰其他 WPF 功能,例如附加属性)。

    除了启用对象元素语法外,还应对将该对象用作其值类型的任何其他公共属性启用属性元素语法。这是因为,对象现在可以实例化为对象元素,而且可以填充此类属性的属性元素值。

    自定义类的属性 (Property) 作为 XAML 属性 (Attribute) 的要求
    属性必须引用按值类型(如基元),或者为在类级别具有默认构造函数或专用类型转换器的类型使用类。

    或者,属性可以引用抽象类类型或接口。对于抽象类或接口,运行时的期望是,属性值必须使用实现该接口的实际类实例或从该抽象类派生的类实例填充。

    属性可以在抽象类上声明,但只能在从抽象类派生的实际类上设置,因为创建类的对象元素完全要求有效的默认构造函数和可实例化的类。

    启用了类型转换器的属性语法
    如果在类级别提供专用的属性化类型转换器,则应用的类型转换将为任何需要实例化该类型的属性启用属性语法。类型转换器不启用该类型的对象元素用法;只有当存在该类型的默认构造函数时才会启用对象元素用法。所以,启用了类型转换器的属性一般而言在属性语法中不可用,除非该类型本身也支持对象元素语法。这一点的一个例外是,可以指定属性元素语法,但允许该属性元素包含一个字符串。该用法实质上相当于属性语法用法,这样的用法不常见,除非需要对属性值进行更可靠的空白处理。例如,以下用法是接受字符串的属性 (property) 元素用法和等效的属性 (attribute) 用法:

     
    允许使用属性语法,但通过 XAML 禁止使用包含对象元素的属性元素语法的属性示例有各种接受 Cursor 类型的属性。Cursor 类有专用类型转换器 CursorConverter,但未公开默认构造函数,因此 Cursor 属性只能通过属性语法进行设置,即使实际 Cursor 类型为引用类型。

    每个属性类型转换器
    此外,属性本身也可以在属性级别声明类型转换器。对于基于适当类型的 ConvertFrom 操作,通过将属性 (attribute) 的传入字符串值作为输入进行处理,这将启用“mini language”,它将实例化内联属性 (property) 类型的对象。通常,这样做是为了提供方便的访问器,而不是作为在 XAML 中设置属性的唯一手段。但是,对于想要使用现有 CLR 类型(不提供默认构造函数或属性化类型转换器)的属性,也可以使用类型转换器。例如,WPF  API 中接受 CultureInfo 类型的某些属性。在此情况下,WPF 使用现有的 Microsoft .NET Framework CultureInfo 类型来更好地处理与早期版本框架的兼容性以及早期版本框架中使用的迁移方案,但 CultureInfo 类型不支持将必要的构造函数或类型级别的类型转换直接用作 XAML 属性值。

    公开具有 XAML 用法的属性时,特别当您是控件作者时,尤其应考虑使用依赖项属性支持该属性。使用 XAML 处理器的现有 Windows Presentation Foundation (WPF) 实现时更是如此,因为使用 DependencyProperty 支持可以提高性能。对于用户期望的 XAML 可访问属性,依赖项属性将公开该属性的属性系统功能。这些功能包括动画、数据绑定和样式支持。

    编写和属性化类型转换器
    您可能经常需要编写自定义 TypeConverter 派生类,以便为属性类型提供类型转换。

    有关自定义类事件的 XAML 事件处理程序属性语法的要求
    若要将事件用作 CLR 事件,必须在支持默认构造函数的类上或可以在派生类中访问事件的抽象类上,将该事件公开为公共事件。为了可方便地用作路由事件,CLR 事件应实现显式 add 和 remove 方法,这两种方法分别添加和移除 CLR 事件签名的处理程序,并将这些处理程序转发到 AddHandler 和 RemoveHandler 方法。这些方法在事件所附加到的实例的路由事件处理程序存储区中添加或删除处理程序。

    说明:
    可以使用 AddHandler 直接注册路由事件的处理程序,而不用特意定义用于公开路由事件的 CLR 事件。通常建议不要这样做,因为该事件不会对附加处理程序启用 XAML 属性语法,并且所生成的类将为类对象模型提供不够透明的 XAML 视图。

    编写集合属性
    接受集合类型的属性具有的 XAML 语法允许您指定要添加到集合的对象。此语法有两种显著功能。

    不需要在对象元素语法中指定属于集合对象的对象。如果在 XAML 中指定接受集合类型的属性,则隐式存在该集合类型。

    该集合属性的子元素将被处理为集合的成员。通常,代码对集合成员的访问通过集合方法(如 Add)或集合索引器属性执行。但 XAML 语法不支持方法或索引器。对于生成元素树的操作,集合明显是很常见的要求,您需要在声明性 XAML 中通过某种方法填充这些集合。因此,处理集合属性的子元素的方法是将这些子元素添加到将作为集合属性类型值的集合中。

    WPF XAML 处理器对集合属性的构成内容使用以下定义。属性的属性类型必须实现以下内容之一:

    实现 IList。

    实现 IDictionary 或等效泛型 (IDictionary<(Of <(TKey, TValue>)>))。

    从 Array 派生
    实现 IAddChild(WPF 定义的接口)。

    这每一种类型都具有 Add 方法,XAML 处理器使用该方法向基础集合中添加项。

    说明:
    由 WPF XAML 处理器执行的集合检测功能不支持泛型列表和字典接口(IList<(Of <(T>)>) 和 IDictionary<(Of <(TKey, TValue>)>))。但是,可以将 List<(Of <(T>)>) 类用作基类(因为它直接实现 IList),或者将 Dictionary<(Of <(TKey, TValue>)>) 用作基类(因为它直接实现 IDictionary)。

    声明接受集合的属性时,务必注意在该类型的新实例中初始化属性值的方式。如果未将属性实现为依赖项属性,则使属性使用可调用集合类型构造函数的支持字段是合适的。如果属性是依赖项属性,则可能需要将集合属性初始化为默认类型构造函数的一部分。这是因为依赖项属性从元数据接受其默认值,您通常不会希望集合属性的初始值是静态的共享集合(每个包含类型实例都应有集合实例)。

    您可以为集合属性实现自定义集合类型。由于集合属性隐式进行处理,因此自定义集合类型不需要提供默认构造函数就可以在 XAML 中隐式使用。但是,也可以选择为集合类型提供默认构造函数。这可能是一种值得的做法,因为除非确实提供了默认构造函数,否则不能显式将集合声明为对象元素。一些标记作者可能喜欢将显式集合视作一种标记样式。另外,在创建将集合类型用作属性值的新对象时,默认构造函数可以简化初始化要求。

     声明 XAML 内容属性
    XAML 语言定义了 XAML 内容属性的概念。对象语法中可用的每个类恰好有一个 XAML 内容属性。若要将属性声明为类的 XAML 内容属性,请将 ContentPropertyAttribute 作为类定义的一部分进行应用。在属性中将要使用的 XAML 内容属性的名称指定为 Name。

    您可以将集合属性指定为 XAML 内容属性。这将导致使用该属性,由此对象元素可以有一个或多个子元素,而没有任何插入集合对象元素或属性元素标记。这些元素然后被作为 XAML 内容属性的值进行处理,并添加到支持集合实例中。

    一些现有的 WPF XAML 内容属性使用 Object 的属性类型。这将使 XAML 内容属性可接受基元值(如 String),并可接受单个引用对象值。如果遵从此模型,则您的类型将负责类型确定和可能类型的处理。使用 Object 类型模型的一般原因有两种,一种是支持将对象内容添加为字符串的简单方式(接受默认呈现处理),另一种是支持添加对象内容(指定非默认呈现)的高级方式。

     序列化 XAML
    对于某些情况,例如如果您是控件作者,则可能还需要确保 XAML 中可实例化的任何对象表示形式也可以反序列化到等效 XAML。本主题中不介绍序列化要求。

    1. 标记扩展和XAML

    本主题介绍可扩展应用程序标记语言 (XAML) 的标记扩展概念,包括其语法规则、用途以及底层的类对象模型。

     XAML 处理器和标记扩展
    XAML 处理器是指可根据其规范(通过编译或解释)将 XAML 接受为语言、并且可以生成结果基础类以供运行时对象模型使用(也是根据 XAML 规范)的任意程序。默认情况下,此类处理器要么将属性值解释为一个文本字符串,要么基于属性类型或该属性特定的类型转换器将该属性值转换为对象。不过,有时也存在要求其他行为的情况。例如,可能指示 XAML 处理器:某个属性的值应该是对已构造对象或静态对象的引用。或者指示 XAML 处理器使用向对象的构造函数提供非默认参数的语法。相对于指定的 XAML 处理器默认行为,这是一种反常行为。

     基本标记扩展语法
    可以实现标记扩展以便为属性 (Attribute) 用法中的属性 (Property) 和/或属性 (Property) 元素用法中的属性 (Property) 提供值。

    当用于提供属性 (Attribute) 值时,将标记扩展与 XAML 处理器区分开来的语法就是左右大括号({ 和 })。然后,由紧跟在左大括号后面的字符串标记来标识标记扩展的类型。

    当用在属性元素语法中时,标记扩展在外观上与其他任何用于提供属性元素值的元素相同,即:一个将标记扩展类作为一个元素引用并以尖括号 (<>) 括起的 XAML 元素声明。

     特定于 WPF 的标记扩展
    WPF 编程中最常用的标记扩展是支持资源引用的标记扩展(StaticResource 和 DynamicResource)以及支持数据绑定的标记扩展 (Binding)。

    StaticResource 通过替换已定义资源的值来为 XAML 属性提供值。

    DynamicResource 通过将值推迟为对资源的运行时引用来为 XAML 属性提供值。动态资源引用强制在每次访问此类资源时都重新进行查找。

    Binding 按应用于元素的数据上下文来为属性提供数据绑定值。此标记扩展相对复杂,因为它会启用大量内联语法来指定数据绑定。

    RelativeSource 为可以在运行时元素树中定位若干可能关系的 Binding 提供源信息。对于在多用途模板中创建的绑定,或在未充分了解周围的元素树的情况下以代码创建的绑定,上述标记扩展会提供专用源。

    通过 TemplateBinding,控件模板可以使用来自要利用该模板的类的对象模型定义属性中的模板化属性的值。

    XAML 定义的标记扩展
    有几个标记扩展并非是 XAML 的 WPF 应用程序所特有的,而是属于 XAML 语言的规范和命名空间的一部分。它们通常由语法中的 x: 前缀标识,如您在常见用法中所见到的一样。这些标记扩展的 WPF 实现使用相同的 MarkupExtension 基类来提供实现。

    说明:
    x: 前缀在 XAML 应用程序或文档的根元素中用于 XAML 命名空间的典型命名空间映射。例如,Microsoft Visual Studio 2005 模板使用此 x: 映射启动 XAML 文件。您可以在自己的 xmlns 映射中选择一个不同的前缀标记,但是本文档采用默认的 x: 映射,作为标识那些属于 XAML 命名空间已定义部分的实体的一种方式,这与 WPF 命名空间或其他任意命名空间是相反的。

    x:Type 为命名类型提供 Type 对象。此标记扩展最常用于样式和模板。

    x:Static 从不直接属于属性值类型、但可以计算为该类型的值类型代码实体中生成静态值。

    x:Null 将 null 指定为 XAML 属性的值。

    在特意不使用基元素和控件模型提供的集合支持的情况下,x:Array 为 XAML 语法中常规数组的创建提供支持。

    有关标记扩展语法的更多信息

    *Extension 类

    每个标记扩展的行为都会通过从 MarkupExtension 派生的 *Extension 类通知给 XAML 处理器,并提供 ProvideValue 方法的实现。每个扩展的此方法都会定义在计算标记扩展后将返回哪个对象。通常,通过使用传递给标记扩展的各个字符串标记来对返回的对象进行实例化或设置。

    例如,StaticResourceExtension 类提供实际资源查找的图面实现,以便其 ProvideValue 实现返回请求的对象,该特定实现的输入是用于按其 x:Key 查找资源的字符串。如果您使用的是现有标记扩展,则其中的大部分实现详细信息都无关紧要。

    后续字符串标记的扩展类解释
    跟在标记扩展标识符后面、并且仍在括号内的字符串标记由 XAML 处理器通过以下方式之一解释:

    逗号始终代表各个标记的分隔符。因此,文本逗号无法传递给标记扩展。

    如果各个分隔的标记不包含任何等号,则每个标记都将被视为构造函数参数。每个构造函数参数都必须按该签名所期望的类型给出,并按照该签名所期望的顺序排列。

    说明:
    XAML 处理器必须调用与对的数量这一参数计数匹配的构造函数。为此,如果您要实现自定义标记扩展,请不要提供具有相同实参计数的多个形参;对于当存在多个具有相同形参计数的标记扩展构造函数时将发生的情况,尚未定义相应的行为。

    如果各个分隔的标记包含等号,则 XAML 处理器会首先为标记扩展调用默认构造函数。之后,每个“名称=值”对都会被解释为标记扩展上存在的属性名称以及赋给该属性的值。

    如果在标记扩展中的构造函数行为与属性设置行为之间存在并行结果,则您使用哪个行为都无关紧要。如果仅仅因为将“属性=值”对用于具有多个可设置属性的标记扩展就可以令您的标记意图性更强,并减小意外转置构造函数参数的可能性(当您指定“属性=值”对时,这些属性可以为任意顺序),那么这种用法更常见。另外,无法保证标记扩展提供设置每个可设置属性的构造函数参数。例如,Binding 是一个标记扩展,具有多个可以通过“属性=值”形式的扩展来设置的属性,但 Binding 仅支持两个构造函数,即默认构造函数和一个设置初始路径的构造函数。

    转义文本大括号
    XAML 处理器中的属性处理使用大括号作为标记扩展的指示符。必要时,还可以使用后面跟文本大括号的空大括号对来输入转义序列,从而生成文本大括号字符属性值。

    嵌套扩展语法
    支持多个标记扩展的嵌套,并且将首先计算每个标记扩展的最里层,例如:

    < P Property="Background">

    Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" />

    有关标记扩展和属性元素语法的更多信息

    当用作填写属性元素值的对象元素时,标记扩展类在外观上与可用在 XAML 中的普通元素没有区别。这种情况下,普通元素与标记扩展之间的实际差异就在于标记扩展要么可计算为一个类型值,要么延迟为一个表达式,因此属性值的任何可能的类型错误的机制都将是不同的,这与后期绑定属性在其他编程模型中的处理方式类似。普通元素将计算为类型,而属性是在编译时直接设置的。

    当用在对象元素语法中以填充属性元素时,大部分标记扩展都不会包含内容或任何进一步的属性元素语法,这样您就可以关闭对象元素标记,而不提供任何子元素。不论何时 XAML 处理器遇到任何对象元素,都会调用该类的构造函数以实例化从已分析元素创建的对象。标记扩展类也一样;因此,如果您希望自己的标记扩展在对象元素语法中可用,就必须提供默认构造函数。有些现有标记扩展至少具有一个必需的属性值,为了使初始化有效,必须指定该值。在这种情况下,通常会将该属性值作为对象元素的属性特性来指定。在 XAML 命名空间 (x:) 语言功能和 WPF 命名空间 XAML 扩展参考页中,将指出具有必需属性的标记扩展(以及必需属性的名称)。参考页还将指出特定标记扩展是否禁止使用对象元素语法或属性语法。一个值得注意的情况是 x:Array 标记扩展,它无法支持属性语法,因为必须指定该数组的内容。数组内容的处理方式与常规对象一样,因此属性没有默认类型转换器是可行的。另外,x:Array 标记扩展需要 Type 参数。

    1. XAML命名空间和命名空间映射

    本主题进一步解释每个可扩展应用程序标记语言 (XAML) 文件的根标记中存在的两个命名空间映射及其用途,同时还介绍如何生成类似的映射,以便使用在您自己的代码中和/或单独的程序集中定义的元素。

    WPF 和 XAML 命名空间声明
    在许多可扩展应用程序标记语言 (XAML) 文件的根标记中的命名空间声明内,您都可以看到两个 xmlns 声明。第一个声明将整个 Windows Presentation Foundation (WPF) 命名空间映射为默认命名空间:

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    第二个声明映射单独的可扩展应用程序标记语言 (XAML) 命名空间,通常将其映射为 x: 前缀。

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    这些声明之间的关系是:XAML 实际上是语言标准,而 WPF 是将 XAML 作为语言使用的一个实现。XAML 语言指定一些为了兼容而假定要实现的语言元素,每个元素都应当能通过针对 XAML 命名空间执行的 XAML 处理器实现进行访问。WPF 实现为其自己的 API 保留默认命名空间,为 XAML 中需要的标记语法使用单独的映射前缀。按照约定,该前缀是 x:,此 x: 约定后面是项目模板、示例代码和此 SDK 中语言功能的文档。XAML 命名空间定义了许多常用功能,这些功能即使对于基本的 WPF 应用程序也是必需的。例如,若要通过分部类将任何代码隐藏加入 XAML 文件,您必须将该类命名为相关 XAML 文件的根元素中的 x:Class 属性。或者,在 XAML 页中定义的、您希望作为键控资源访问的任何元素应当对相关元素设置了 x:Key 属性。

    映射到自定义类和程序集
    您可以在 xmlns 前缀声明中使用一系列标记将命名空间映射到程序集,这与将标准 WPF 和 XAML 命名空间映射到前缀类似。

    语法使用下列可能的命名标记和值:

    clr-namespace: 在包含要作为元素公开的公共类型的程序集中声明的公共语言运行库 (CLR) 命名空间。

    assembly= 是指包含部分或全部引用的 CLR 命名空间的程序集。该值通常只是程序集的名称,而不是路径。该程序集的路径必须在生成编译的 XAML 的项目文件中以项目引用的形式建立。另外,为了合并版本管理和强名称签名,该值也可以是 AssemblyName 定义的字符串。

    请注意,分隔 clr-namespace 标记和其值的字符是冒号 (:),而分隔 assembly 标记和其值的字符是等号 (=)。这两个标记之间使用的字符是分号。例如:

    xmlns:custom="clr-namespace:SDKSample;assembly=SDKSampleLibrary"

    映射到当前程序集
    如果引用的 clr-namespace 是在引用自定义类的应用程序代码所在的程序集中定义的,则可以省略 assembly。这种情况的等效语法是指定 assembly=,等号后不需要任何字符串标记。

    如果自定义类是在同一程序集中定义的,则不能将其用作页的根元素。不需要映射分部类;如果您希望在 XAML 中将自定义类作为元素引用,只需要映射应用程序中页的非分部类。

     在程序集中将 CLR 命名空间映射为 XML 命名空间
    WPF 定义一个 CLR 属性,XAML 处理器使用它来将多个 CLR 命名空间映射到单个 XML 命名空间。XmlnsDefinitionAttribute 属性放置在生成程序集的源代码中的程序集级别。WPF 程序集源代码使用此属性将各种常见的命名空间(如 System.Windows 和 System.Windows.Controls)映射到 http://schemas.microsoft.com/winfx/2006/xaml/presentation 命名空间。

    XmlnsDefinitionAttribute 采用两个参数:XML 命名空间名称和 CLR 命名空间名称。可以存在多个 XmlnsDefinitionAttribute,这样便可以将多个 CLR 命名空间映射到同一 XML 命名空间。映射后,如果需要,还可以通过在分部类代码隐藏页中提供相应的 using 语句来引用这些命名空间的成员,而无需完全限定。

    1. WPF名称范围

    名称范围既是一种概念,也是用于存储对象的 XAML 定义名称及其实例等效项之间的关系的编程对象。加载 XAML 应用程序的页面时,即在 WPF 托管代码中创建了名称范围。作为编程对象的名称范围由 INameScope 接口定义,并且还由实际类 NameScope 实现。

     加载的 XAML 应用程序中的名称范围
    处理 XAML 页时,即对该页的根元素创建了名称范围。该页中指定的每个名称都会添加到相关的名称范围中。作为常见根元素(例如 Page 和 Window)的元素总是控制名称范围。如果在标记中某个元素(例如 FrameworkElement 或 FrameworkContentElement)是页的根元素,则 XAML 处理器会隐式添加一个 Page 根元素,以便 Page 可以提供一个名称范围。即使最初在 XAML 中没有定义 Name 或 x:Name 属性,也会创建一个名称范围。

    如果试图在任意名称范围中两次使用同一个名称,则会引发异常。对于具有代码隐藏并且是已编译的应用程序的一部分的 XAML,在创建页的已生成类时会引发该异常。

    将元素添加到已分析的元素树
    若要在初始的加载和处理之后向元素树添加任何元素,都必须对定义名称范围的类调用相应的 RegisterName 的实现。否则,无法通过 FindName 等方法按名称引用添加的对象。仅设置 Name 属性(或 x:Name 属性)不会将该名称注册到任何名称范围中。将命名的元素添加到具有名称范围的元素树中也不会将此名称注册到名称范围中。尽管名称范围可以嵌套,但通常您应该将名称注册到根元素上存在的名称范围中,这样您的名称范围位置便可与在等效的加载 XAML 页中可能已创建的名称范围并列。 应用程序开发人员最常用的方案是使用 RegisterName 将名称注册到当前根元素的名称范围中。RegisterName 是查找将作为动画运行的演示图板的一种重要方案的一部分。有关更多信息,请参见演示图板概述。如果您对同一逻辑树中的非根元素的元素调用 RegisterName,则该名称仍然会注册到最靠近根元素的元素,就好像对该根元素调用了 RegisterName 一样。

    代码中的名称范围
    对于以编程方式创建(而不是来自加载的 XAML)的应用程序,若要支持名称范围,根元素必须实现 INameScope、或者必须是 FrameworkElement 或 FrameworkContentElement 派生类。

    此外,对于不是由 XAML 处理器加载和处理的任何元素,默认情况下不会创建或初始化该对象的名称范围。必须为随后要向其中注册名称的任何元素显式创建新的名称范围。若要为元素创建名称范围,可调用静态 SetNameScope 方法。将该元素指定为 dependencyObject 参数,并且将新的 NameScope 构造函数调用指定为 value 参数。

    如果作为 SetNameScope 的 dependencyObject 提供的对象不是 INameScope 实现,也不是 FrameworkElement 或 FrameworkContentElement,那么,对任何子元素调用 RegisterName 均无效。如果您无法显式创建新的名称范围,则调用 RegisterName 将引发异常。

     样式和模板中的名称范围
    WPF 中的样式和模板提供了以简单的方法重新使用和重新应用内容的功能,但样式和模板可能还包括具有在模板级别指定的名称的元素。同一个模板可能在某个页中被多次重复使用。因此,样式和模板都定义其自己的名称范围,而与样式或模板所应用到的包含页无关。

    请看下面的示例:

    此处,同一个模板被应用到两个不同的按钮。如果模板没有独立的名称范围,则该模板中用到的 TheBorder 名称会导致名称冲突。模板的每个实例化都有其自已的名称范围,因此在本示例中每个实例化模板的名称范围都只包含一个名称。

    样式也具有其自己的名称范围,因此大多数情况下演示图板的某些部分会分配有特殊的名称。即使模板重新定义为控件自定义项的一部分,这些名称也会启用以具有此名称的元素为目标的控件特定行为。

    由于名称范围是独立的,因此在模板中查找命名的元素比在页中查找非模板化的元素更具有挑战性。首先需要确定所应用的模板,方法是获取该模板所应用到的控件的 Template 属性值。然后,调用 FindName 的模板版本,将模板所应用到的控件作为第二个参数进行传递。

    如果您是控件作者,您要生成一个约定,即所应用的模板中的特定命名元素是控件本身所定义的行为的目标,则可以在控件实现代码中使用 GetTemplateChild 方法。由于 GetTemplateChild 方法是受保护的,因此只有控件作者才可以访问它。

    如果您正在使用模板,并且需要用到该模板所应用到的名称范围,则获取 TemplatedParent,然后调用其中的 FindName。举一个使用模板的例子:您正在编写事件处理程序实现,其中该事件将从所应用的模板中的元素被引发。

     名称范围和名称相关的 API
    FrameworkElement 具有 FindName、RegisterName 和 UnregisterName 方法。如果您对其调用这些方法的元素拥有名称范围,则这些元素方法仅调入名称范围方法。否则,会查看其父元素是否具有名称范围,此进程会以递归方式继续执行,直到找到一个名称范围为止(由于 XAML 处理器行为的原因,可以保证在根元素有名称范围)。FrameworkContentElement 具有类似的行为,所不同的是 FrameworkContentElement 从不会有名称范围。在 FrameworkContentElement 上存在上述方法,因此这些调用最终可以转发到 FrameworkElement 父元素。

    SetNameScope 用于将新的名称范围映射到现有的对象。您可以多次调用 SetNameScope 以重置或清除名称范围,但这种用法并不常见。此外,通常不通过代码使用 GetNameScope。

    名称范围实现
    下面的类直接实现 INameScope:

    NameScope

    Style

    ResourceDictionary

    FrameworkTemplate

    ResourceDictionary 不使用名称范围,而是使用键,原因是它是一种字典哈希表实现。ResourceDictionary 之所以实现 INameScope,其唯一的原因是它可以对用户代码引发异常,从而有助于澄清真正的名称范围和 ResourceDictionary 处理键的方式之间的区别,此外,还可以保证名称范围不会被父元素特别应用到 ResourceDictionary。

    FrameworkTemplate 和 Style 通过显式接口定义实现 INameScope。这些显式实现允许这些名称范围在通过 INameScope 接口进行访问时表现为常规行为,这是名称范围与 WPF 内部进程进行通信的方式。但是,这些显式接口定义不是 FrameworkTemplate 和 Style 的常规名称范围的一部分,原因是几乎不需要对 FrameworkTemplate 和 Style 直接调用 INameScope 方法。

    下面的类都定义自己的名称范围,方法是使用 System.Windows..::.NameScope 帮助器类并通过 NameScope 附加属性连接到相应的名称范围实现:

    FrameworkElement

    FrameworkContentElement

    WPF控件开发

    1. WPF控件开发之控件概述

    Windows Presentation Foundation (WPF) 附带了许多几乎可以在所有 Windows 应用程序中使用的常见 UI 组件,其中包括 Button、Label、TextBox、Menu 和 ListBox。以前,这些对象被称为控件。不过,WPF SDK 仍继续使用术语“控件”,泛指任何代表应用程序中可见对象的类。请注意,类不必从 Control 类继承,即可具有可见外观。从 Control 类继承的类包含一个 ControlTemplate,允许控件的使用方在无需创建新子类的情况下根本改变控件的外观。 本主题讨论在 WPF 中使用控件(包括从 Control 类继承的控件以及不从该类继承的控件)的常见方式。

     创建控件的实例
    可以通过使用可扩展应用程序标记语言 (XAML) 或以代码形式向应用程序添加控件。 下面的示例演示如何创建一个向用户询问其姓名的简单应用程序。 此示例在 XAML 中创建六个控件:两个标签、两个文本框及两个按钮。所有控件都可以按相似的方式创建。

    下面的示例以代码的形式创建同一应用程序。为了简单起见,示例中不包括 Grid grid1 的创建。grid1 与前面的 XAML 示例内容具有相同的列和行定义。

     

    更改控件外观
    更改控件的外观以适应应用程序的外观,这是很常见的操作。可以根据您要达到的效果,通过执行以下操作之一来更改控件的外观:

    更改控件的属性值。

    为控件创建 Style。

    为控件创建新 ControlTemplate。

    更改控件的属性值
    许多控件具有允许您更改控件外观的属性,例如 Button 的 Background。可以在 XAML 和代码中设置值属性。下面的示例在 XAML 中设置 Button 的 Background、FontSize 和 FontWeight 属性。

    下面的示例在代码中设置相同的属性。

      

    为控件创建样式
    利用 WPF,通过创建 Style,您可以同时为许多控件指定相同的外观,而不是在应用程序中设置每个实例的属性。 下面的示例创建一个 Style,它可以应用于应用程序中的每个 Button。Style 定义通常是在 ResourceDictionary(例如 FrameworkElement 的 Resources)中以 XAML 形式定义的。

    通过将键分配给样式并在控件的 Style 属性中指定该键,还可将样式仅应用于某些特定类型的控件。

    创建 ControlTemplate
    利用 Style,可以一次为多个控件设置属性,但有时除了通过创建 Style 可执行的操作之外,您可能想要自定义 Control 的外观。从 Control 类继承的类具有 ControlTemplate,它用于定义 Control 的结构和外观。Control 的 Template 属性是公共的,因此您可以为 Control 指定非默认 ControlTemplate。通常,您可以为 Control 指定新的 ControlTemplate(而不是从控件继承)以自定义 Control 的外观。

    请考虑一个很常用的控件 Button。 Button 的主要行为是当用户单击它时让应用程序实现某些操作。 默认情况下,WPF 中的 Button 显示为一个凸出的矩形。 开发应用程序时,您可能会希望利用 Button 的行为(即通过处理按钮的单击事件),不过,除了通过更改按钮的属性可以执行的操作外,您也可以更改按钮的外观。 在这种情况下,您可以创建新 ControlTemplate。

    下面的示例创建 Button 的 ControlTemplate。 ControlTemplate 创建一个带圆角和渐变背景的 Button。 ControlTemplate 包含一个 Border,其 Background 为 LinearGradientBrush,它具有两个 GradientStop 对象。 第一个 GradientStop 使用数据绑定将 GradientStop 的 Color 属性绑定到按钮背景的颜色。 当设置 Button 的 Background 属性时,该值的颜色将用作第一个 GradientStop。此示例还创建一个 Trigger,用于在 IsPressed 为 true 时更改 Button 的外观。

      

    说明:
    为使此示例正常工作,Button 的 Background 属性必须设置为 SolidColorBrush。

     订阅事件
    您可以通过使用 XAML 或代码来订阅控件的事件,但只能在代码中处理事件。 下面的示例演示如何订阅 Button 的 Click 事件。

    下面的示例处理 Button 的 Click 事件。

    控件中的丰富内容
    从 Control 类继承的大多数类具有包含丰富内容的能力。例如,Label 可以包含任意对象,例如字符串、Image 或 Panel。 下列类支持丰富内容,可以用作 WPF 中大多数控件的基类。

    ContentControl - 从此类继承的类的部分示例包括有 Label、Button 和 ToolTip。

    ItemsControl - 从此类继承的类的部分示例包括有 ListBox、Menu 和 StatusBar。

    HeaderedContentControl - 从此类继承的类的部分示例包括有 TabItem、GroupBox 和 Expander。

    HeaderedItemsControl - 从此类继承的类的部分示例包括有 MenuItem、TreeViewItem 和 ToolBar。

    1. 使用XAML创建按钮

    本演练的目的在于介绍如何创建要在 Windows Presentation Foundation (WPF) 应用程序中使用的动画按钮。本演练使用样式和模板来创建自定义的按钮资源,通过该按钮资源可以重用代码并将按钮逻辑与按钮声明分开。本演练完全是用可扩展应用程序标记语言 (XAML) 编写的。
    重要说明:
    本演练指导您通过在 Microsoft Visual Studio 中键入或复制和粘贴可扩展应用程序标记语言 (XAML) 来逐步创建应用程序。

    下图显示的是已完成的按钮。

     创建基本按钮
    让我们首先从创建一个新项目并向窗口中添加几个按钮开始。

    创建新的 WPF 项目并向窗口中添加按钮
    启动 Visual Studio。

    创建新的 WPF 项目:在“文件”菜单上,指向“新建”,再单击“项目”。此时将打开下图所示的窗口。在左窗格中的“Visual C#”节点下面,选择“.NET Framework 3.0”。在右窗格中,选择“Windows 应用程序 (WPF)”模板。将该项目命名为“AnimatedButton”。这将创建应用程序的主干。

    添加基本的默认按钮:本演练中所需的全部文件均由该模板提供。通过在解决方案资源管理器中双击 Window1.xaml 文件来打开它。默认情况下,Window1.xaml 中存在一个 Grid 元素。移除 Grid 元素,并通过向 Window1.xaml 中键入或复制和粘贴以下突出显示的代码来向可扩展应用程序标记语言 (XAML) 页面中添加几个按钮:

    按 F5 运行该应用程序;您应当能够看到类似下图的一组按钮。

    现在您已经创建了基本按钮,这样就完成了在 Window1.xaml 文件中的工作。本演练其余部分重点介绍如何在 app.xaml 文件中为这些按钮定义样式和模板。

    设置基本属性
    接着,我们将会为这些按钮设置一些属性,以便控制按钮外观和布局。您将使用资源来为整个应用程序定义按钮属性,而不是为这些按钮单独设置属性。在概念上,应用程序资源与网页的外部级联样式表 (CSS) 相似;但是,资源远比级联样式表 (CSS) 强大,在本演练结束时您将明白这一点。

    使用样式为按钮设置基本属性
    定义 Application.Resources 块:打开 app.xaml 并添加下面突出显示的标记(如果尚未添加):    

    资源范围由资源的定义位置来确定。如果资源是在 app.xaml 文件的 Application.Resoureses 中定义的,则将允许从应用程序中的任何位置使用资源。

    创建一个样式并用该样式定义基本属性值:向 Application.Resources 块添加下面的标记。此标记创建一个应用于该应用程序中所有按钮的 Style,并将这些按钮的 Width 设置为 90,将 Margin 设置为 10:

    TargetType 属性指定将该样式应用于 Button 类型的所有对象。每个 Setter 都为 Style 设置不同的属性值。因此,此时该应用程序中的每个按钮的宽度都为 90,边距都为 10。如果您按 F5 运行该应用程序,则将看到下面的窗口。

    您还可以对样式进行更多的处理,这包括以各种方式微调对象的目标、指定复杂的属性值,甚至将样式作为其他样式的输入。

    为资源设置样式属性值:使用资源,可以通过一种简单的方式来重用通常定义的对象和值。为了使代码更加模块化,使用资源定义复杂值尤其有用。向 app.xaml 添加下面突出显示的标记。

    您已经在 Application.Resources 块的正下方创建了一个名为“GrayBlueGradientBrush”的资源。此资源将定义一个水平渐变。此资源可以在该应用程序中的任何位置(包括在 Background 属性的按钮样式 setter 内部中)用作属性值。现在,所有的按钮都具有此渐变的 Background 属性值。

    按 F5 运行该应用程序。其外观类似于下图:

    创建一个定义按钮外观的模板
    在本节中,将创建一个用来自定义按钮外观(表示)的模板。按钮表示是由几个赋予按钮独特外观的对象(包括矩形和其他组件)组成的。

    到目前为止,对应用程序中按钮外观的控制已限制为更改按钮的属性。如果您希望更彻底地改变按钮的外观,该怎么办?使用模板可以强有力地控制对象的表示。由于模板可以在样式中使用,因此您可以将模板应用于所有应用了样式的对象(在本演练中为按钮)。

    使用模板定义按钮的外观
    设置模板:由于控件(如 Button)具有 Template 属性,因此您可以像对 Style 中所设置的其他属性值那样,使用 Setter 来定义模板属性值。向按钮样式中添加下面突出显示的标记。

      

    更改按钮表示:此时,您需要定义模板。添加下面突出显示的标记。此标记指定了两个后跟 DockPanel 且具有圆角的 Rectangle 元素。DockPanel 用于承载按钮的 ContentPresenter。A ContentPresenter 显示按钮的内容。本演练中,内容为文本 ("Button 1", "Button 2", "Button 3")。所有模板组件(矩形和 DockPanel)都放置在一个 Grid 内。

    按 F5 运行该应用程序。其外观类似于下图:

    向模板中添加玻璃效果:接着将添加玻璃效果。首先创建一些用来生成玻璃渐变效果的资源。将这些渐变资源添加到 Application.Resources 块中的任何位置:

    这些资源将用作按钮模板的 Grid 中所插入的矩形的 Fill。向模板添加下面突出显示的标记。

    请注意,x:Name 属性为“glassCube”的矩形的 Opacity 为 0,因此,当您运行该示例时,将看到上面并没有覆盖玻璃矩形。这是由于我们将在以后向该模板中添加用户与该按钮交互时将触发的触发器。但是,您可以通过将 Opacity 值更改为 1 并运行该应用程序来查看按钮现在的外观。请参见下图。在转至下一步之前,请将 Opacity 更改回 0。

    创建按钮交互性
    在本节中,将创建属性触发器和事件触发器来更改属性值和运行动画,以响应用户操作(如将鼠标指针移到按钮上并单击)。

    添加交互性(鼠标悬停、鼠标离开和单击等)的一个简便方法就是在模板或样式内部定义触发器。若要创建 Trigger,需要定义一个属性“条件”,例如:按钮的 IsMouseOver 属性值等于 true。随后将定义在触发条件为 true 时所发生的 setter(操作)。

    创建按钮交互性
    添加模板触发器:向模板添加突出显示的标记。

    添加属性触发器:向 ControlTemplate.Triggers 块添加突出显示的标记:

    按 F5 运行应用程序,并查看在将鼠标指针移到按钮上时的效果。

    添加焦点触发器:接着,将添加一些类似的 setter 来处理当按钮具有焦点时(例如,当用户单击按钮之后)的情况。

    按 F5 运行应用程序,并单击其中的某个按钮。请注意,在单击该按钮之后,该按钮仍具有焦点,因此它仍将保持突出显示状态。如果您单击另一个按钮,则新按钮将获得焦点,而上一个按钮则失去焦点。

    为 MouseEnter 和 MouseLeave 添加动画:接着将向触发器添加一些动画。在 ControlTemplate.Triggers 块内部的任意位置添加下面的标记。

    当鼠标指针移到该按钮上时,玻璃矩形会收缩;当指针离开该按钮时,镜面矩形会返回到其正常大小。

    当指针移到该按钮上(引发 MouseEnter 事件)时会触发两个动画。这些动画会使玻璃矩形沿着 X 轴和 Y 轴收缩。请注意 DoubleAnimation 元素的属性:Duration 和 By。Duration 指定动画持续半秒多,By 指定玻璃收缩 10%。

    第二个事件触发器 (MouseLeave) 只是停止第一个触发器。当您停止某个 Storyboard 时,所有的动画属性都恢复到其默认值。因此,当用户将指针移开按钮时,该按钮将返回到鼠标指针移到其上之前的状态。

    添加单击按钮时的动画效果:最后一步是添加在用户单击按钮时将触发的触发器。在 ControlTemplate.Triggers 块内部的任意位置添加下面的标记:

    按 F5 运行应用程序,并单击其中的某个按钮。当您单击某个按钮时,玻璃矩形会旋转。

    摘要

    在本演练中,进行了下列练习:

    使 Style 面向对象类型 (Button)。

    使用 Style 控制整个应用程序中按钮的基本属性。

    创建要用于 Style setter 的属性值的资源(如渐变)。

    通过向按钮应用模板来自定义整个应用程序中按钮的外观。

    自定义按钮的行为(包括动画效果)来响应用户操作(如 MouseEnter、MouseLeave 和 Click)。

    1. WPF控件库之Button

    Button 控件响应来自鼠标、键盘、手写笔或其他输入设备的用户输入,并引发一个 Click 事件。Button 是一个基本的用户界面 (UI) 组件,可以包含简单内容(例如文本),也可以包含复杂内容,例如图像和 Panel 控件。

    处于默认状态、具有焦点和处于按下状态的按钮

    示例
    下面的示例创建两个 Button 控件。一个 Button 包含文本,另一个包含图像。当用户单击包含图像的 Button 时,另一个 Button 的背景和文本将发生变化。

    此示例使用标记创建 Button 控件,但使用代码编写 Click 事件处理程序。

    1. WPF控件库之Menu

    Menu 是一个控件,使用该控件可以对那些与命令或事件处理程序相关联的元素以分层方式进行组织。每个 Menu 可以包含多个 MenuItem 控件。每个 MenuItem 都可调用命令或调用 Click 事件处理程序。MenuItem 也可以有多个 MenuItem 元素作为子项,从而构成子菜单。

    下图演示了菜单控件的三种不同状态。默认状态是没有设备(如鼠标指针)停留在 Menu 上时的状态。当鼠标指针悬停在 Menu 上时显示焦点状态,当在 Menu 上单击鼠标按钮时显示按下状态。

    不同状态下的菜单

    使用 Menu 类,可以按照分层顺序对与命令和事件处理程序相关联的元素进行组织。每个 Menu 元素都包含一个由 MenuItem 元素组成的集合。

    Menu 控件
    Menu 控件表示一系列用来为应用程序指定命令或选项的项。通常,单击 MenuItem 将打开一个子菜单或者导致应用程序执行相应的命令。

    创建 Menu
    下面的示例将创建一个 Menu 来操作 TextBox 中的文本。Menu 包含多个使用 Command、IsCheckable 和 Header 属性以及 Checked、Unchecked 和 Click 事件的 MenuItem 对象。

    具有键盘快捷键的 MenuItem
    键盘快捷键是可以用键盘输入以调用 Menu 命令的字符组合。例如,“复制”的快捷键是 Ctrl+C。可以将以下两个属性与键盘快捷键和菜单项结合使用:InputGestureText 和 Command。

    InputGestureText
    下面的示例演示如何使用 InputGestureText 属性来将键盘快捷键文本分配给 MenuItem 控件。这只是将键盘快捷键放在菜单项中, 而不会将命令与 MenuItem 相关联。应用程序必须处理用户的输入才能执行该操作。

     

    命令
    下面的示例演示如何使用 Command 属性来将“打开”和“保存”命令与 MenuItem 控件相关联。该 Command 属性不但将命令与 MenuItem 相关联,而且还提供要用作快捷方式的输入笔势文本。

    MenuItem 类还有一个 CommandTarget 属性,该属性指定要在其中执行命令的元素。如果未设置 CommandTarget,则具有键盘焦点的元素将收到命令。

    Menu 样式
    使用控件样式设置,可以显著改变 Menu 控件的外观和行为,而不必编写自定义控件。除了设置可视化属性外,还可以向控件的各个部分应用 Style,通过属性更改控件各个部分的行为,向控件中添加额外的部分,或者改变控件的布局。下面的示例演示了几种用来将 Style 添加到 Menu 控件中的方法。

    第一个代码示例定义了一个名为 Simple 的 Style 来说明如何在您的样式中使用当前的系统设置。该代码将 MenuHighlightBrush 的颜色分配为菜单的背景色,将 MenuTextBrush 分配为菜单的前景色。请注意,您在分配画笔时使用的是资源键。

    下面的示例使用的是 Trigger 元素,通过这些元素,可以改变 MenuItem 的外观来响应发生在 Menu 上的事件。当您将鼠标移到 Menu 上时,菜单项的前景色和字体特征会发生变化。

    1. WPF控件库之Lable

    Label 控件通常在用户界面 (UI) 中提供信息。一直以来,Label 只包含文本,但由于 Windows Presentation Foundation (WPF) 附带的 Label 是一个 ContentControl,所以它可以包含文本或 UIElement。

    Label 为快捷键提供功能性和可视化支持。它常用于实现对控件(如 TextBox)的快速键盘访问。若要为 Control 指定 Label,请将 Label..::.Target 属性设置为当用户按下快捷键时应获得焦点的控件。

    下图演示了一个目标为 ComboBox 的 Label“主题”。当用户按下 Alt+T 时,ComboBox 将获得焦点。

    示例

    下面的示例演示如何创建一个 Label,该控件使用 AccessText 并且绑定到目标 TextBox。

     

     

    示例
    为标签添加文本换行

    Label 控件不支持文本换行。如果您需要一个多次换行的标签,可以嵌套一个支持文本换行的元素,并将该元素放在标签内。下面的示例演示如何使用 TextBlock 创建一个进行多次文本换行的标签。

     

     

    为标签添加访问键和文本换行

    如果您需要一个具有访问键(助记键)的 Label,则可以使用 Label 中的 AccessText 元素。

    Label、Button、RadioButton、CheckBox、MenuItem、TabItem、Expander 和 GroupBox 等控件具有默认的控件模板。这些模板包含一个 ContentPresenter。您可以为 ContentPresenter 设置的属性之一是 RecognizesAccessKey="true",您可以使用该属性为控件指定访问键。

    下面的示例演示如何创建一个具有访问键并支持文本换行的 Label。为了实现文本换行,本示例设置了 TextWrapping 属性并使用下划线字符指定访问键。(紧跟下划线字符后面的字符就是访问键。)

    1. WPF控件库之Toolbar

    ToolBar 控件是一组通常在功能上相关的命令或控件的容器。

    下面的插图显示垂直和水平 ToolBar 控件。

    水平 Toolbar

    垂直 Toolbar

    ToolBar 控件
    ToolBar 控件因其按钮或其他控件像条形栏一样排列成一行或一列而得名。WPF ToolBar 控件提供一种溢出机制,将不能自然适合于有大小限制的 ToolBar 的任意项放入一个特殊的溢出区域。另外,WPF ToolBar 控件通常还与相关的 ToolBarTray 控件一起使用,后者提供特殊的布局行为,并支持用户启动的工具栏大小调整和排列。

    在 ToolBarTray 中指定工具栏的位置
    使用 Band 和 BandIndex 属性可以在 ToolBarTray 中定位 ToolBar。Band 指示 ToolBar 在其父 ToolBarTray 中的位置。BandIndex 指示 ToolBar 放入其 Band 中的顺序。下面的示例演示如何使用此属性在 ToolBarTray 内放置 ToolBar 控件。

      

    具有溢出项的工具栏 
    通常,ToolBar 控件包含的项多于工具栏大小可以容纳的项数。出现此情况时,ToolBar 会显示一个溢出按钮。要查看溢出项,用户可以单击溢出按钮,这些项即会显示在 ToolBar 下方的弹出窗口中。下图显示了一个带溢出项的 ToolBar。

    具有溢出项的工具栏


    通过将 ToolBar..::.OverflowMode 附加属性设置为 OverflowMode..::.Always、OverflowMode..::.Never 或 OverflowMode..::.AsNeeded,您可以指定工具栏上的项何时会放置在溢出面板上。下面的示例指定工具栏上的最后四个按钮应总是显示在溢出面板上。

    ToolBar 在其 ControlTemplate 中使用 ToolBarPanel 和 ToolBarOverflowPanel。 ToolBarPanel 负责工具栏上的项的布局。 ToolBarOverflowPanel 负责 ToolBar 上容不下的项的布局。

    1. WPF控件开发之自定义控件

    Windows Presentation Foundation (WPF) 控件模型的扩展性极大减少了创建新控件的需要。但在某些情况下,仍可能需要创建自定义控件。本主题讨论可最大限度减少在 Windows Presentation Foundation (WPF) 中创建自定义控件以及其他控件创作模型的需要的功能。本主题还演示如何创建新控件。

    编写新控件的替代方法
    以前,如果要通过现有控件获取自定义体验,您只能更改控件的标准属性,例如背景色、边框宽度和字号。如果希望在这些预定义参数的基础之上扩展控件的外观或行为,则需要创建新的控件,通常的方法是继承现有控件并重写负责绘制该控件的方法。虽然这仍是一种可选方法,但也可以利用 WPF 丰富内容模型、样式、模板和触发器来自定义现有的控件。下面的列表提供了一些示例,演示如何在不创建新控件的情况下使用这些功能来实现统一的自定义体验。

    丰富内容。 很多标准 WPF 控件支持丰富内容。例如,Button 的内容属性为 Object 类型,因此从理论上讲,任何内容都可以显示在 Button 上。若要让按钮显示图像和文本,可以将图像和 TextBlock 添加到 StackPanel 中,然后将 StackPanel 分配给 Content 属性。由于这些控件可以显示 WPF 可视化元素和任意数据,因此,减少了创建新控件或修改现有控件来支持复杂可视化效果的需要。

    样式。 Style 是表示控件属性的值的集合。使用样式可创建所需控件外观和行为的可重用表示形式,而无需编写新控件。例如,假设希望所有 TextBlock 控件都呈现字号为 14 的红色 Airal 字体。您可以创建一个样式作为资源,然后相应地设置适当的属性。这样,添加到应用程序中的每个 TextBlock 都将具有相同的外观。

    数据模板。 DataTemplate 可用于自定义数据在控件上的显示方式。例如,DataTemplate 可用于指定数据在 ListBox 中的显示方式。有关这种情况的示例,请参见数据模板概述。除了自定义数据外观之外,DataTemplate 还可以包含 UI 元素,这样大大增加了自定义 UI 的灵活性。例如,使用 DataTemplate 可以创建一个 ComboBox,其中每一项都包含一个复选框。

    控件模板 WPF 中的很多控件都使用 ControlTemplate 来定义控件的结构和外观,这样可将控件外观和控件功能分离开。通过重新定义控件的 ControlTemplate,可以彻底更改控件的外观。例如,假设您希望控件看起来像一个交通信号灯。此控件具有简单的用户界面和功能。该控件有三个圆形,一次只能点亮其中的一个。经过考虑之后,您可能意识到 RadioButton 提供了一次只选中一项的功能,但是 RadioButton 的默认外观完全不像交通信号灯上的灯。由于 RadioButton 使用控件模板来定义其外观,因此很容易重新定义 ControlTemplate 以符合该控件的要求,从而使用单选按钮来制作交通信号灯。

    说明:
    尽管 RadioButton 可以使用 DataTemplate,但在本例中,只使用 DataTemplate 还不够。DataTemplate 定义控件内容的外观。对于 RadioButton,指示 RadioButton 是否选中的那个圆形右侧显示出来的全部都是该控件的内容。在交通信号灯的示例中,单选按钮只需要成为可“点亮”的圆形。由于交通信号灯的外观要求与 RadioButton 的默认外观存在很大差异,因此,有必要重新定义 ControlTemplate。一般而言,DataTemplate 用于定义控件的内容(或数据),ControlTemplate 用于定义控件的构成方式。

    触发器。 Trigger 用于在不创建新控件的情况下动态更改控件的外观和行为。例如,假设应用程序中有多个 ListBox 控件,而您希望每个 ListBox 中的项在选中时都显示为红色粗体。您首先想到的可能是创建一个从 ListBox 继承的类,然后重写 OnSelectionChanged 方法,以更改选中项的外观,不过,更好的方法是向 ListBoxItem 的样式添加一个更改选中项外观的触发器。触发器用于更改属性值或根据属性值执行操作。EventTrigger 用于在发生事件时执行操作。

    一般而言,如果控件完全复制现有控件的功能,但您希望该控件具有不同的外观,则应先考虑是否可以使用本节中讨论的某些方法来更改现有控件的外观。

    控件创作模型
    通过丰富内容模型、样式、模板和触发器,最大程度地减少了创建新控件的需要。但是,如果确实需要创建新控件,那么理解 WPF 中的不同控件创作模型就显得非常重要。WPF 提供三个用于创建控件的一般模型,每个模型都提供不同的功能集和灵活度。这三个模型的基类分别为 UserControl、Control 和 FrameworkElement。

    从 UserControl 派生
    在 WPF 中创建控件的最简单方法是从 UserControl 派生。如果生成继承自 UserControl 的控件,需要将现有组件添加到 UserControl,命名这些组件,然后在 可扩展应用程序标记语言 (XAML) 中引用事件处理程序。执行这些操作之后,即可在代码中引用这些命名元素和定义事件处理程序。此开发模型非常类似于用于 WPF 应用程序开发的模型。

    如果生成正确,UserControl 可以利用丰富内容、样式和触发器的优点。但是,如果控件继承自 UserControl,则使用该控件的用户将无法使用 DataTemplate 或 ControlTemplate 来自定义其外观。因此,有必要从 Control 类或其派生类(UserControl 除外)进行派生,以便创建支持模板的自定义控件。

    从 UserControl 派生的优点
    如果符合以下所有情况,请考虑从 UserControl 派生:

    希望以类似于生成应用程序的方式生成控件。

    控件仅由现有组件组成。

    不需要支持复杂自定义项。

    从 Control 派生
    从 Control 类派生是大多数现有 WPF 控件使用的模型。在创建继承自 Control 类的控件时,可使用模板定义其外观。通过这种方式,可以将运算逻辑从可视化表示形式中分离出来。通过命令和绑定(而不是事件),也可确保分离 UI 和逻辑,尽可能避免引用 ControlTemplate 中的元素。如果控件的 UI 和逻辑正确分离,该控件的用户即可重新定义其 ControlTemplate,从而自定义其外观。尽管生成自定义 Control 不像生成 UserControl 那样简单,自定义 Control 还是提供了最大的灵活性。

    从 Control 派生的优点
    如果符合以下任一情况,请考虑从 Control 派生,而不要使用 UserControl 类:

    希望控件外观能通过 ControlTemplate 进行自定义。

    希望控件支持不同的主题。

    从 FrameworkElement 派生
    从 UserControl 或 Control 派生的控件依赖于组合现有元素。很多情况下,这是一种可接受的解决方案,因为从 FrameworkElement 继承的任何对象都可以位于 ControlTemplate 中。但是,某些时候,简单的元素组合不能满足控件的外观需要。对于这些情况,使组件基于 FrameworkElement 才是正确的选择。

    生成基于 FrameworkElement 的组件有两种标准方法:直接呈现和自定义元素组合。直接呈现涉及的操作包括:重写 FrameworkElement 的 OnRender 方法,并提供显式定义组件视觉效果的 DrawingContext 操作。此方法由 Image 和 Border 使用。自定义元素组合涉及的操作包括使用 Visual 类型的对象组合组件的外观。有关示例,请参见使用 DrawingVisual 对象。Track 是 WPF 中使用自定义元素组合的控件示例。在同一控件中,也可以混合使用直接呈现和自定义元素组合。

    从 FrameworkElement 派生的优点
    如果符合以下任一情况,请考虑从 FrameworkElement 派生:

    希望对控件的外观进行精确控制,而不仅仅是简单的元素组合提供的效果。

    想要通过定义自己的呈现逻辑来定义控件的外观。

    想要以一种 UserControl 和 Control 之外的新颖方式组合现有元素。

     控件创作基础知识
    如前所述,WPF 最强大的功能之一是,无需创建自定义控件即可实现远比设置控件的基本属性来更改其外观和行为更强的功能。WPF 属性系统和 WPF 事件系统使样式、数据绑定和触发器功能成为可能。如果在控件中实现依赖项属性和路由事件,则无论使用什么模型创建自定义控件,自定义控件的用户都可以像对 WPF 随附的控件那样使用这些功能。

    使用依赖项属性
    当属性为依赖项属性时,可以进行下面的操作:

    在样式中设置该属性。

    将该属性绑定到数据源。

    使用动态资源作为该属性的值。

    动画处理该属性。

    如果希望控件的属性支持以上任一功能,则应将该属性实现为依赖项属性。下面的示例定义一个依赖项属性。

    这段代码执行下面的操作:

    将一个名为 ValueProperty 的 DependencyProperty 标识符定义为 publicstaticreadonly 字段。

    通过调用 DependencyProperty..::.Register 向属性系统注册该属性名,以指定以下内容:

    属性的名称。

    属性的类型。

    拥有该属性的类型。

    属性的元数据。元数据包含该属性的默认值、CoerceValueCallback 和 PropertyChangedCallback。

    通过实现该属性的 get 和 set 访问器,定义一个名为 Value 的 CLR“包装”属性,这个名称也就是用来注册该依赖项属性的名称。请注意,get 和 set 访问器只是分别调用 GetValue 和 SetValue。建议依赖项属性的访问器不要包含其他逻辑,这是因为客户端和 WPF 可绕过这两个访问器直接调用 GetValue 和 SetValue。例如,如果属性绑定到数据源,则不会调用该属性的 set 访问器。不要向 get 和 set 访问器添加其他逻辑,而应使用 ValidateValueCallback、CoerceValueCallback 和 PropertyChangedCallback 委托在值更改时进行响应或检查该值。

    为 CoerceValueCallback 定义一个名为 CoerceValue 的方法。CoerceValue 确保 Value 大于或等于 MinValue 且小于或等于 MaxValue。

    为 PropertyChangedCallback 定义一个名为 OnValueChanged 的方法。OnValueChanged 创建一个 RoutedPropertyChangedEventArgs<(Of <(T>)>) 对象,并准备引发 ValueChanged 路由事件。路由事件在下一节中讨论。

    使用 RoutedEvent
    就像依赖项属性以附加功能扩展 CLR 属性的用途一样,路由事件扩展了标准 CLR 事件的用途。在创建新的 WPF 控件时,将事件实现为路由事件也是一种好方法,这是因为路由事件支持以下行为:

    事件可以在多个控件的父级上进行处理。如果事件是冒泡事件,则元素树中的单个父级可预订该事件。然后,应用程序作者可以使用一个处理程序来响应多个控件的该事件。例如,如果控件属于 ListBox 中的每个项(因为它包含在 DataTemplate 中),则应用程序开发人员可以为该控件的 ListBox 事件定义相应的事件处理程序。每当这些控件中的任何控件发生该事件时,都会调用该事件处理程序。

    路由事件可在 EventSetter 中使用,应用程序开发人员通过 EventSetter 可以在样式内指定事件的处理程序。

    路由事件可在 EventTrigger 中使用,这对于使用 XAML 对属性进行动画处理很有用。

    下面的示例定义了一个路由事件。

    这段代码执行下面的操作:

    将一个名为 ValueChangedEvent 的 RoutedEvent 标识符定义为 publicstaticreadonly 字段。

    通过调用 EventManager..::.RegisterRoutedEvent 方法注册路由事件。该示例在调用 RegisterRoutedEvent 时指定以下信息:

    事件的名称为 ValueChanged。

    路由策略为 Bubble,这意味着首先调用源(引发事件的对象)上的事件处理程序,然后从最近的父元素上的事件处理程序开始,相继调用源的各个父元素上的事件处理程序。

    该事件处理程序的类型为 RoutedPropertyChangedEventHandler<(Of <(T>)>),是用 Decimal 类型构造的。

    该事件的所属类型为 NumericUpDown。

    声明一个名为 ValueChanged 的公共事件,并包含事件访问器声明。该示例调用 add 访问器声明中的 AddHandler 和 remove 访问器声明中的 RemoveHandler 来使用 WPF 事件服务。

    创建一个名为 OnValueChanged 的受保护的虚方法,该方法引发 ValueChanged 事件。

    使用绑定
    若要将控件 UI 与其逻辑分离,请考虑使用数据绑定。如果是使用 ControlTemplate 定义控件的外观,这一点尤其重要。使用数据绑定时,可能能够避免引用 UI 的特定部分。

    下面的示例更新 NumericUpDown 控件的 TextBlock,向它分配一个名称,然后在代码中按名称引用该文本框。

    下面的示例使用绑定来达到相同的目的。

    针对设计器设计
    若要在 Visual Studio Windows Presentation Foundation (WPF) 设计器中获得对自定义 WPF 控件的支持(例如,使用“属性”窗口编辑属性),请遵循以下准则。

    依赖项属性
    确保实现 CLRget 访问器和 set 访问器,如前面“使用依赖项属性”中所述。设计器可以使用包装来检测某个依赖项属性是否存在,但与 WPF 和控件客户端一样,在获取或设置属性时不需要使用设计器来调用访问器。

    附加属性
    应按照以下原则在自定义控件上实现附加属性:

    具有一个使用 RegisterAttached 方法创建的 publicstaticreadonlyDependencyProperty,其形式为“属性名称Property”。传递到 RegisterAttached 的属性名称必须与属性名称 匹配。

    实现一对名为 Set属性名称 和 Get属性名称 的 public static CLR 方法。这两种方法都应接受从 DependencyProperty 派生的类作为其第一个参数。Set属性名称 方法还接受其类型与属性的注册数据类型匹配的参数。Get属性名称 方法应返回相同类型的值。如果缺少 Set属性名称 方法,则该属性标记为只读。

    Set 属性名称 和 Get属性名称 必须分别直接路由到目标依赖项对象的 GetValue 和 SetValue 方法。通过调用方法包装或直接调用目标依赖项对象,设计器可以访问附加属性。

    创建 UserControl
    如前所述,在 WPF 中创建控件的最简单方法是从 UserControl 派生。下面的示例演示用于定义 NumericUpDownUserControl 的 用户界面 (UI) 的 XAML:

    下面的示例演示此 UserControl 的逻辑。

    如此示例所示,自定义 UserControl 的开发模型非常类似于用于应用程序开发的模型。

    创建自定义控件
    生成支持模板的控件
    UserControl 提供了一种简单方法在 WPF 中生成可重用的功能,但要利用模板化和支持不同主题,则要使用的模型为 Control。本节将上一节中的 UserControl 示例转换为自定义 Control。

    更改基类
    首先,将 UserControl 基类替换为 Control。

    移动到模板
    一旦更新了基类,则需要将控件的内容移动到模板。模板在可位于应用程序中的很多位置的样式中定义。对于此示例,样式位于应用程序资源中。

    在 UserControl 示例中,TextBlock 和 RepeatButton 实例已指定了名称。RepeatButton 实例还引用了代码中定义的事件处理程序。可以在此自定义 Control 中移除这两个实例,因为将通过更松散耦合的方式,改为使用绑定和命令来获取相同的行为。

    处理输入
    在 UserControl 示例中,RepeatButton 实例直接引用了代码中定义的事件处理程序。对于自定义 Control,命令是实现相同行为的一种更灵活的方式。控件可以定义命令,如下面的示例所示。

    然后,模板中的元素可以引用这些命令,如下面的示例所示。

    通过定义模板以及使用绑定和命令,您已将 NumericUpDown 控件从具有固定可视化效果的静态 UserControl 更改为可灵活自定义的自定义 Control。

    外部控件库
    最后一步是将 NumericUpDown 控件打包到其自己的程序集中,以使重用更容易。

    创建主题文件
    将 NumericUpDown 类移动到库程序集之后,需要移动样式定义。首先,需要创建一个“themes”文件夹用于存储所有主题文件。接下来,创建一个名为 generic.xaml 的文件。此文件将用作此程序集的所有资源查找的回退。

    在 generic.xaml 中定义一个 ResourceDictionary,并在 ResourceDictionary 中放置 NumericUpDown 控件的样式。

    定义 ThemeInfo 属性
    若要找到 generic.xaml 中的 Style,宿主应用程序需要知道该程序集包含控件特定的资源。可以通过向类中添加程序集属性来实现该任务。由于程序集仅包含一般资源,因此将 GenericDictionaryLocation 属性设置为 SourceAssembly。下面的示例显示了 AssemblyInfo.cs 文件的内容。

    为 Windows 主题提供支持
    一个控件在大量不同 WPF主题中可能有不同的外观。若要支持多个主题,必须使用控件所需的正确样式、模板和其他资源来定义主题文件。还需要将 ThemeInfoAttribute 属性 (Attribute) 的 ThemeDictionaryLocation 属性 (Property) 设置为引用源程序集,如下面的示例所示。

    本文档概述在设计可方便地样式化和模板化的控件时需要考虑的一组最佳做法。在为内置的 WPF 控件集处理主题控件样式时,我们通过大量试验和错误总结出了这组最佳做法。我们已经认识到,成功的样式化不只是设计完善的对象模型的功能,也是样式本身的功能。本文档面向控件作者,而不是样式作者。

    术语
    “样式化和模板化”是一组技术,控件作者可以通过该组技术来将控件的可视化特性延迟到控件的样式和模板。这组技术包括:

    样式(包括属性 setter、触发器和演示图板)。

    资源。

    控件模板。

    数据模板。

    准备工作:了解您的控件
    在开始阅读本文档中的准则之前,一定要了解并定义了控件的一般用法,这一点很重要。样式化公开一组通常不受约束的可能性。旨在由许多开发人员在许多应用程序中广泛使用的控件面临着如下挑战:可以使用样式化来对控件的可视化外观进行广泛的更改。实际上,带有样式的控件甚至可能并非控件作者的本意。由于样式化在本质上可以提供无限的灵活性,因此您可以使用“一般用法”这一概念来帮助您限制自己的决定。

    为了了解控件的一般用法,最好考虑控件的价值主张。您的控件能够提供哪些无法由其他控件提供的内容? 一般用法并不表示任何特定的可视化外观,而是表示控件的基本原理和一组有关其用法的合理预期。了解到这一点,就可以对控件在一般情况下的撰写模型和样式定义行为进行一些假设。例如,对于 ComboBox,如果您了解其一般用法,并不意味着您非常清楚特定的 ComboBox 是否有圆角,但是您将由此清楚 ComboBox 可能需要一个弹出窗口和某种用来切换其开关状态的方法。

    通用准则
    不严格实施模板约定。 控件的模板约定可以包含元素、命令、绑定和触发器,甚至还可以包含必需的或为了使控件正常工作而应当使用的属性设置。

    尽可能减少约定。

    围绕如下预期来设计:在设计时(即,在使用设计工具时),控件模板通常处于不完整状态。WPF 不提供“正在撰写”状态的基础结构,因此,控件必须围绕这样的状态可能有效这一预期来生成。

    在没有遵循模板约定的任何方面时,不引发异常。按照这一原则,当面板的子级太多或太少时,面板不应当引发异常。

    将外围功能分解成模板帮助器元素。 每个控件都应当将重点放在其核心功能和真正的价值主张上,而且每个控件都应当由控件的一般用法来定义。为此,请使用模板中的撰写和帮助器元素来实现外围行为和可视化,即,那些不构成控件核心功能的行为和可视化。帮助器元素可分为三类:

    独立帮助器类型是以“匿名方式”用在模板中的可重用的公共控件或基元,这意味着帮助器元素和带有样式的控件无法互相识别。在技术上,任何元素都可以是匿名类型,但是在这里,术语“独立”用于描述那些封装专用功能以实现目标方案的类型。

    基于类型的帮助器元素是封装专用功能的新类型。通常,这些元素在设计上比公用控件或基元的功能范围要窄。与独立帮助器元素不同的是,基于类型的帮助器元素能够识别它们的使用上下文,而且通常必须与包含它们所属模板的控件共享数据。

    命名的帮助器元素是控件应当能够在其模板中根据名称找到的公用控件或基元。这些元素在模板中具有一个已知的名称,这使得控件能够找到这些元素并以编程方式与之交互。在任何模板中,都只能有一个具有给定名称的元素。

    下表显示的是由目前的控件样式使用的部分帮助器元素列表:

    尽可能减少帮助器元素所必需的、特定于用户的绑定或属性设置。通常,帮助器元素需要某些绑定或属性设置才能在控件模板中正确工作。帮助器元素和模板化控件应当尽可能多地生成这些设置。在设置属性或者建立绑定时,注意不要重写由用户设置的值。具体的最佳做法如下所示:

    命名的帮助器元素应当由父级标识,而且父级应当针对帮助器元素建立任何必需的设置。

    对于基于类型的帮助器元素,应当直接针对它们建立任何必需的设置。这样做可能需要帮助器元素查找它在使用时的信息上下文,包括其 TemplatedParent(它在使用时的模板的控件类型)。例如,在用于 ContentControl 派生类型时,ContentPresenter 会自动将它的 TemplatedParent 的 Content 属性绑定到它的 Content 属性。

    独立帮助器元素不能按这种方式进行优化,这是因为按照定义,帮助器元素和父级不能相互识别。

    使用 Name 属性来标记模板中的元素。如果控件需要在样式中查找某个元素才能以编程方式访问它,则该控件应当使用 Name 属性和 FindName 范例来进行查找。控件不应在未找到所需元素时引发异常,而是应在不提示的情况下禁用需要该元素的功能。

    使用最佳做法来表示样式中的控件状态和行为。 下面按顺序列出了用来表示样式中的控件状态更改和行为的最佳做法。您应当使用列表上的第一项来实现您的方案。

    属性绑定。示例:在 ComboBox..::.IsDropDownOpen 和 ToggleButton..::.IsChecked 之间绑定。

    触发的属性更改或属性动画。示例:Button 的悬停状态。

    命令。示例:ScrollBar 中的 LineUpCommand / LineDownCommand。

    独立帮助器元素。示例:TabControl 中的 TabPanel。

    基于类型的帮助器类型。示例:Button 中的 ContentPresenter 和 Slider 中的 TickBar。

    命名的帮助器元素。示例:ComboBox 中的 TextBox。

    命名帮助器类型中的冒泡事件。如果您侦听样式元素中的冒泡事件,则应当要求生成该事件的元素能够进行唯一标识。示例:ToolBar 中的 Thumb。

    自定义 OnRender 行为。示例:Button 中的 ButtonChrome。

    慎用样式触发器(与模板触发器相对)。会影响到模板中元素属性的触发器必须在模板中进行声明。会影响到控件属性(没有 TargetName)的触发器可以在样式中声明,除非您知道更改模板还可能会损坏触发器。

    与现有的样式模式一致。 一个问题常常有多种解决办法。注意尽可能与现有的控件样式模式保持一致。这对于派生自同一基类型(例如,ContentControl、ItemsControl、RangeBase 等)的控件尤其重要。

    在不重新模板化的情况下公开属性来启用常见自定义项方案。WPF 不支持可插入/可自定义的部件,因此控件用户只能使用两种自定义方法:直接设置属性或者使用样式设置属性。请记住,比较合适的做法是,设置数量有限的属性,使其面向极其常见的高优先级自定义项方案,否则的话,这些方案需要重新模板化。下面是有关何时以及如何启用自定义项方案的最佳方法:

    极其常见的自定义项应当作为属性在控件上公开并由模板使用。

    不太常见(尽管并非极少见)的自定义项应当作为附加属性公开并由模板使用。

    需要对已知但是极少见的自定义项重新模板化,这一点也是可以接受的。

    主题注意事项
    主题样式应当尝试在所有的主题之间具有一致的属性语义,但不保证能够实现这一点。作为控件文档的一部分,控件应当具有一个描述其属性语义(即控件属性的“含义”)的文档。例如,ComboBox 控件应当定义 Background 属性在 ComboBox 中的含义。控件的默认样式应当尝试遵循在其文档中的所有主题中定义的语义。另一方面,控件用户应当注意属性语义可能因主题而异。在某些情况下,给定的属性在由特定主题所需的可视化约束下可能无法表示 (例如,对于许多控件来说,传统主题没有可以向其应用 Thickness 的边框)。

    主题样式不需要在所有的主题之间具有一致的触发器语义。由控件样式通过触发器或动画公开的行为可能因主题而异。控件用户应当注意到,控件将不必使用同一个机制在所有的主题中实现一个特定的行为。例如,一个主题可以使用动画来表示悬停行为,而另一个主题则可以使用触发器。这可能会导致自定义控件上的行为保留出现不一致现象 (例如,如果控件的悬停状态是使用触发器来表示的,那么,更改背景属性可能不会影响该状态)。但是,如果悬停状态是使用动画来实现的,那么,更改背景属性可能会不可挽回地中断动画,从而中断状态过渡)。

    主题样式不需要在所有的主题之间具有一致的“布局”语义。例如,默认的样式不需要保证控件将在所有的主题中占用同样的大小,也不需要保证控件将在所有的主题中具有同样的内容边距/空白。

    1. WPF控件开发之装饰器

    装饰器是一种特殊类型的 FrameworkElement,用于向用户提供可视化提示。对于其他用户,装饰器可用于将功能控点添加到元素中或提供有关控件的状态信息。

     关于装饰器
    Adorner 是绑定到 UIElement 的自定义 FrameworkElement。装饰器在 AdornerLayer 中呈现,这是一个始终位于装饰元素或装饰元素集合上方的呈现图面。装饰器的呈现独立于该装饰器所绑定到的 UIElement 的呈现。装饰器通常使用位于装饰元素左上部的标准 2-D 坐标原点,相对于其绑定到的元素进行定位。

    装饰器的常见应用包括:

    向 UIElement 添加功能控点,使用这些控点,用户可以通过某种方式(调整大小、旋转、重新定位等等)操作元素。

    提供可视反馈以指示各种状态,或响应各种事件。

    在 UIElement 上叠加视觉效果。

    从视觉上遮盖或重写 UIElement 的一部分或全部。

    Windows Presentation Foundation (WPF) 为装饰视觉元素提供一个基本框架。下表列出了装饰对象时使用的主要类型及其用途。下面是几个用法示例。

    Adorner
    一个抽象基类,所有具体装饰器的实现都从该类继承。

    AdornerLayer
    一个类,表示一个或多个装饰元素的装饰器的呈现层。

    AdornerDecorator
    一个类,使装饰器层与元素集合相关联。

     实现自定义装饰器
    Windows Presentation Foundation (WPF) 提供的装饰器框架主要用于支持创建自定义装饰器。您可以通过实现一个从抽象 Adorner 类继承的类来创建自定义装饰器。

    说明:
    Adorner 的父级为 AdornerLayer,它呈现的是 Adorner,而不是正在装饰的元素。

    下面的示例演示一个实现简单装饰器的类。该示例装饰器使用圆简单地装饰 UIElement 的各角。

    下面的图像演示了应用于 TextBox 的 SimpleCircleAdorner。

     装饰器的呈现行为
    注意:装饰器不包括任何继承呈现行为,确保装饰器呈现是装饰器实施者的责任。 实现呈现行为的常见方式是重写 OnRenderSizeChanged 方法,并使用一个或多个 DrawingContext 对象来按需呈现装饰器的视觉效果(如上述示例所示)。

    说明:
    放置在装饰器层中的所有内容均呈现在您已设置的任何其余样式的顶部。也就是说,装饰器始终以可见的方式位于顶部,无法使用 z 顺序重写。

    事件和命中测试
    装饰器就像任何其他 FrameworkElement 一样接收输入事件。 因为装饰器的 z 顺序总是高于它所装饰的元素,所以该装饰器接收可能适用于基础装饰元素的输入事件(例如 Drop 或 MouseMove)。 装饰器可以侦听某些输入事件,并通过重新引发这些事件将其传递给基础装饰元素。

    若要对装饰器下的元素启用传递命中测试,请在装饰器上将命中测试 IsHitTestVisible 的属性设置为 false。

    装饰单个 UIElement
    若要将装饰器绑定到特定的 UIElement,请按照以下步骤操作:

    调用静态方法 GetAdornerLayer,为要装饰的 UIElement 获取一个 AdornerLayer 对象。GetAdornerLayer 从指定的 UIElement 开始沿着可视树向上走,返回它所发现的第一个装饰器层。(如果未发现装饰器层,则该方法返回 Null。)

    调用 Add 方法将装饰器绑定到目标 UIElement。

    以下示例将 SimpleCircleAdorner(如上述所示)绑定到名为 myTextBox 的 TextBox。

    说明:
    目前不支持使用可扩展应用程序标记语言 (XAML) 将装饰器绑定到另一个元素。

    装饰面板的子级
    若要将装饰器绑定到 Panel 的子级,请按照下列步骤操作:

    调用 static 方法 GetAdornerLayer 为要绑定其子级的元素查找一个装饰器层。

    依次枚举父元素的子级并调用 Add 方法将装饰器绑定到每个子级元素。

    以下示例将 SimpleCircleAdorner(如上所示)绑定到名为 myStackPanel 的 StackPanel 的子级。

    WPF数据绑定

    1. 数据绑定概述

    Windows Presentation Foundation (WPF) 数据绑定为应用程序提供了一种简单而一致的方法来显示数据以及与数据交互。元素可以以公共语言运行库 (CLR) 对象和 XML 的形式绑定到各种数据源的数据。ContentControl(如 Button)和 ItemsControl(如 ListBox 和 ListView)具有内置功能,使单个数据项或数据项集合可以进行灵活的样式设置。可以在数据之上生成排序、筛选和分组视图。

    WPF 中的数据绑定功能与传统模型相比具有一些优势,包括本质上支持数据绑定的各种属性、灵活的数据 UI 表示形式,以及业务逻辑与 UI 的完全分离。

    本主题首先讨论 WPF 数据绑定的基本概念,然后详细介绍 Binding 类以及数据绑定的其他功能的用法。

    什么是数据绑定?
    数据绑定是在应用程序 UI 与业务逻辑之间建立连接的过程。如果绑定具有正确设置并且数据提供正确通知,则当数据更改其值时,绑定到数据的元素会自动反映更改。数据绑定可能还意味着如果元素中数据的外部表现形式发生更改,则基础数据可以自动更新以反映更改。例如,如果用户编辑 TextBox 元素中的值,则基础数据值会自动更新以反映该更改。

    数据绑定的一种典型用法是将服务器或本地配置数据放置到窗体或其他 UI 控件中。在 WPF 中,此概念得到扩展,包括了大量属性与各种数据源的绑定。在 WPF 中,元素的依赖项属性可以绑定到 CLR 对象(包括 ADO.NET 对象或与 Web 服务和 Web 属性相关联的对象)和 XML 数据。

    有关数据绑定的示例,请看一看来自数据绑定演示的以下应用程序 UI:

    上面是显示拍卖项列表的应用程序 UI。该应用程序演示数据绑定的以下功能:

    ListBox 的内容绑定到 AuctionItem 对象的集合。AuctionItem 对象具有一些属性,如 Description、StartPrice、StartDate、Category、SpecialFeatures。

    ListBox 中显示的数据(AuctionItem 对象)进行模板化,以便显示每个拍卖项的说明和当前价格。这是使用一个 DataTemplate 实现的。此外,每个项的外观取决于要显示的 AuctionItem 的 SpecialFeatures 值。如果 AuctionItem 的 SpecialFeatures 值为 Color,则该项具有蓝色边框。如果该值为 Highlight,则该项具有橙色边框和一个星号。

    用户可以使用提供的 CheckBox 对数据进行分组、筛选或排序。在上面的图像中,选中了“Group by category”(按类别分组)和“Sort by category and date”(按类别和日期排序)CheckBox。您可能已经注意到数据是根据产品类别分组的,而且类别名称按字母顺序排序。这些项在每个类别中也是按照起始日期排序的,虽然从该图像中很难注意到这一点。这是使用集合视图 实现的。

    当用户选中一个项时,ContentControl 会显示选定项的详细信息。这称为主从方案。

    StartDate 属性的类型为 DateTime,该类型返回一个日期,包括精确到毫秒的时间。在此应用程序中,使用了一个自定义转换器,以便显示较短的日期字符串。

    当用户单击“Add Product”(添加产品)按钮时,会出现下面的窗体:

    用户可以编辑窗体中的字段,使用简略预览和详细预览窗格来预览产品清单,然后单击“submit”(提交)以添加新的产品清单。任何现有的分组、筛选和排序功能都会应用于新项。在这种特殊情况下,在上面图像中输入的项会作为 Computer 类别中的第二项显示。

    “Start Date”(起始日期)TextBox 中提供的验证逻辑未在此图像中显示。如果用户输入一个无效日期(无效的格式或过去的日期),则会通过一个 ToolTip 和 TextBox 旁的一个红色感叹号来通知用户。数据验证一节讨论了如何创建验证逻辑。

    在详细介绍数据绑定的上述不同功能之前,我们会先在下一节中讨论一些对理解 WPF 数据绑定非常重要的基本概念。

    基本数据绑定概念

    不论要绑定什么元素,不论数据源的特性是什么,每个绑定都始终遵循下图所示的模型:


    如上图所示,数据绑定实质上是绑定目标与绑定源之间的桥梁。该图演示以下基本的 WPF 数据绑定概念:

    通常,每个绑定都具有四个组件:绑定目标对象、目标属性、绑定源,以及要使用的绑定源中的值的路径。例如,如果要将 TextBox 的内容绑定到 Employee 对象的 Name 属性,则目标对象是 TextBox,目标属性是 Text 属性,要使用的值是 Name,源对象是 Employee 对象。

    目标属性必须为依赖项属性。大多数 UIElement 属性都是依赖项属性,而大多数依赖项属性(除了只读属性)默认情况下都支持数据绑定。(只有 DependencyObject 类型可以定义依赖项属性,所有 UIElement 都派生自 DependencyObject。)

    尽管图中并未指出,但应该注意,绑定源对象并不限于自定义 CLR 对象。WPF 数据绑定支持 CLR 对象和 XML 形式的数据。举例来说,绑定源可以是 UIElement、任何列表对象、与 ADO.NET 数据或 Web 服务关联的 CLR 对象,或是包含 XML 数据的 XmlNode。

    在阅读其他软件开发工具包 (SDK) 主题时,请务必记住一点:当您建立绑定时,您是将绑定目标绑定到 绑定源。例如,如果您要使用数据绑定在一个 ListBox 中显示一些基础 XML 数据,就是将 ListBox 绑定到 XML 数据。

    若要建立绑定,请使用 Binding 对象。本主题的剩余部分会讨论与 Binding 对象相关的许多概念以及该对象的一些属性和用法。

    数据流的方向
    正如上文所述和上图中箭头所示,绑定的数据流可以从数据目标流向数据源(例如,当用户编辑 TextBox 的值时,源值会发生更改)和/或(如果绑定源提供正确的通知)从绑定源流向绑定目标(例如,TextBox 内容会随绑定源中的更改而进行更新)。

    您可能希望应用程序使用户可以更改数据并将数据传播回源对象。或者,您可能不希望允许用户更新源数据。您可以通过设置 Binding 对象的 Mode 属性来对此进行控制。下图演示不同类型的数据流:

    OneWay 绑定导致对源属性的更改会自动更新目标属性,但是对目标属性的更改不会传播回源属性。此绑定类型适用于绑定的控件为隐式只读控件的情况。例如,您可能绑定到如股票行情自动收录器这样的源,或许目标属性没有用于进行更改的控件接口(如表的数据绑定背景色)。如果无需监视目标属性的更改,则使用 OneWay 绑定模式可避免 TwoWay 绑定模式的系统开销。

    TwoWay 绑定导致对源属性的更改会自动更新目标属性,而对目标属性的更改也会自动更新源属性。此绑定类型适用于可编辑窗体或其他完全交互式 UI 方案。大多数属性都默认为 OneWay 绑定,但是一些依赖项属性(通常为用户可编辑的控件的属性,如 TextBox 的 Text 属性和 CheckBox 的 IsChecked 属性)默认为 TwoWay 绑定。确定依赖项属性在默认情况下是单向绑定还是双向绑定的一种编程方式是使用 GetMetadata 获取属性的属性元数据,然后检查 BindsTwoWayByDefault 属性的布尔值。

    OneWayToSource 与 OneWay 绑定相反;它在目标属性更改时更新源属性。一个示例方案是您只需要从 UI 重新计算源值的情况。

    OneTime 绑定未在图中显示,该绑定会导致源属性初始化目标属性,但不传播后续更改。这意味着,如果数据上下文发生了更改,或者数据上下文中的对象发生了更改,则更改会反映在目标属性中。如果您使用的数据的当前状态的快照适于使用,或者这些数据是真正静态的,则适合使用此绑定类型。如果要使用源属性中的某个值初始化目标属性,并且事先不知道数据上下文,则也可以使用此绑定类型。此绑定类型实质上是 OneWay 绑定的简化形式,在源值不更改的情况下可以提供更好的性能。

    请注意,若要检测源更改(适用于 OneWay 和 TwoWay 绑定),则源必须实现一种合适的属性更改通知机制(如 INotifyPropertyChanged)。

    触发源更新的原因
    TwoWay 或 OneWayToSource 绑定侦听目标属性的更改,并将这些更改传播回源。这称为更新源。例如,可以编辑文本框中的文本以更改基础源值。如上一节中所述,数据流的方向由绑定的 Mode 属性的值确定。

    但是,源值是在您编辑文本的同时进行更新,还是在您结束编辑文本并将鼠标指针从文本框移走后才进行更新呢? 绑定的 UpdateSourceTrigger 属性确定触发源更新的原因。下图中右箭头的点演示 UpdateSourceTrigger 属性的角色:


    如果 UpdateSourceTrigger 值为 PropertyChanged,则 TwoWay 或 OneWayToSource 绑定的右箭头指向的值会在目标属性更改时立刻进行更新。但是,如果 UpdateSourceTrigger 值为 LostFocus,则仅当目标属性失去焦点时,该值才会使用新值进行更新。

    与 Mode 属性类似,不同的依赖项属性具有不同的默认 UpdateSourceTrigger 值。大多数依赖项属性的默认值都为 PropertyChanged,而 Text 属性的默认值为 LostFocus。这意味着,只要目标属性更改,源更新通常都会发生,这对于 CheckBox 和其他简单控件很有用。但对于文本字段,每次键击之后都进行更新会降低性能,用户也没有机会在提交新值之前使用退格键修改键入错误。这就是为什么 Text 属性的默认值是 LostFocus 而不是 PropertyChanged 的原因。

    下表使用 TextBox 作为示例提供每个 UpdateSourceTrigger 值的示例方案:

     

    创建绑定

    前面几节中讨论的一些概念可以概括为:使用 Binding 对象建立绑定,每个绑定通常都具有四个组件:绑定目标、目标属性、绑定源、要使用的源值的路径。本节讨论如何设置绑定。

    请看下面的示例,其中的绑定源对象是一个名为 MyData 的类,该类在 SDKSample 命名空间中定义。出于演示的目的,MyData 类具有一个名为 ColorName 的字符串属性,该属性的值设置为“Red”。因此,此示例生成一个具有红色背景的按钮。

    如果将此示例应用于基本关系图,则生成的图如下所示。这是一个 OneWay 绑定,因为 Background 属性在默认情况下支持 OneWay 绑定。

     

    您可能会奇怪为什么会这样,即使 ColorName 属性为类型字符串,而 Background 属性为 Brush 类型。这是由于进行了默认类型转换,此类型转换在数据转换一节中进行了讨论。

    指定绑定源
    请注意,在上一个示例中,绑定源是通过设置 DockPanel 元素上的 DataContext 属性来指定的。Button 随后从 DockPanel(这是其父元素)继承 DataContext 值。在这里重复一下,绑定源对象是绑定的四个必需组件之一。因此,如果未指定绑定源对象,则绑定将没有任何作用。

    可通过多种方法指定绑定源对象。在将多个属性绑定到相同源时,可以使用父元素上的 DataContext 属性。但是,在各个绑定声明上指定绑定源有时可能更为合适。对于上一个示例,可以不使用 DataContext 属性,而是通过在按钮的绑定声明上直接设置 Source 属性来指定绑定源,如下面的示例中所示:

    除了在元素上直接设置 DataContext 属性、从上级继承 DataContext 值(如第一个示例中的按钮)、通过设置 Binding 上的 Source 属性来显式指定绑定源(如最后一个示例中的按钮),还可以使用 ElementName 属性或 RelativeSource 属性指定绑定源。当绑定到应用程序中的其他元素时(例如在使用滑块调整按钮的宽度时),ElementName 属性是很有用的。当在 ControlTemplate 或 Style 中指定绑定时,RelativeSource 属性是很有用的。

    指定值的路径
    如果绑定源是一个对象,则可使用 Path 属性指定要用于绑定的值。如果要绑定到 XML 数据,则可使用 XPath 属性指定该值。在某些情况下,可以使用 Path 属性,即使在数据为 XML 时。例如,如果要访问返回的 XmlNode(作为 XPath 查询的结果)的 Name 属性,则应使用 Path 属性和 XPath 属性。

    请注意,虽然我们已强调要使用的值的 Path 是绑定的四个必需组件之一,但在要绑定到整个对象的情况下,要使用的值会与绑定源对象相同。在这些情况下,不指定 Path 比较合适。请看下面的示例:

    上面的示例使用空绑定语法:{Binding}。在此情况下,ListBox 从父 DockPanel 元素继承 DataContext(此示例中未演示)。当未指定路径时,默认为绑定到整个对象。换句话说,在此示例中路径已被省略,因为要将 ItemsSource 属性绑定到整个对象。

    除了绑定到集合以外,在希望绑定到整个对象,而不是仅绑定到对象的单个属性时,也可以使用此方案。例如,在源对象为类型字符串,并且您仅仅希望绑定到该字符串本身时。另一种常见情况是您希望将一个元素绑定到具有多个属性的一个对象。

    请注意,您可能需要应用自定义逻辑,使数据对您的绑定目标属性有意义。自定义逻辑的形式可以是自定义转换器(如果默认类型转换不存在)。

    Binding 和 BindingExpression
    在详细介绍数据绑定的其他功能和用法之前,介绍 BindingExpression 类会十分有用。如前面几节所述,Binding 类是用于绑定声明的高级别类;Binding 类提供了很多属性,您可以利用这些类来指定绑定的特征。相关类 BindingExpression 是维持源与目标之间的连接的基础对象。一个绑定包含可以在多个绑定表达式之间共享的所有信息。BindingExpression 是无法共享的实例表达式,其中包含有关 Binding 的所有实例信息。

    例如,请看下面的示例,其中 myDataObject 是 MyData 类的实例,myBinding 是源 Binding 对象,MyData 类是包含一个名为 MyDataProperty 的字符串属性的已定义类。此示例将 mytext(TextBlock 的实例)的文本内容绑定到 MyDataProperty。

    您可以使用相同的 myBinding 对象来创建其他绑定。例如,可以使用 myBinding 对象将复选框的文本内容绑定到 MyDataProperty。在这种情况下,将有两个 BindingExpression 实例共享 myBinding 对象。

    可以通过对数据绑定对象调用 GetBindingExpression 的返回值来获取 BindingExpression 对象。以下主题演示 BindingExpression 类的一些用法:

    如何:从绑定目标属性获取绑定对象

    如何:控制文本框文本更新源的时间

    数据转换
    在上面的示例中,按钮是红色的,因为其 Background 属性绑定到一个值为“Red”的字符串属性。可以这样做的原因是 Brush 类型上提供了一个类型转换器,可以将字符串值转换为 Brush。

    要将此信息添加到创建绑定一节的图中,关系图如下所示:

     

    但是,如果绑定源对象不是具有类型字符串的属性,而是具有类型 Color 的 Color 属性,该怎么办? 在这种情况下,为了使绑定正常工作,您需要先将 Color 属性值转换为 Background 属性所接受的值。您需要通过实现 IValueConverter 接口来创建一个自定义转换器,如下面的示例中所示:

    现在使用自定义转换器而不是默认转换,关系图如下所示:

    在这里重复一下,由于要绑定到的类型中提供了类型转换器,因而可以使用默认转换。此行为取决于目标中可用的类型转换器。如果无法确定,请创建您自己的转换器。

    下面提供了一些典型方案,在这些方案中,实现数据转换器是非常有意义的:

    数据应根据区域性以不同方式显示。例如,可能需要根据在特定区域性中使用的值或标准,来实现货币转换器或日历日期/时间转换器。

    使用的数据不一定会更改属性的文本值,但会更改其他某个值(如图像的源,或显示文本的颜色或样式)。在这种情况下,可以通过转换可能不合适的属性绑定(如将文本字段绑定到表单元格的 Background 属性)来使用转换器。

    将多个控件或控件的多个属性绑定到相同数据。在这种情况下,主绑定可能仅显示文本,而其他绑定则处理特定的显示问题,但仍使用同一绑定作为源信息。

    到目前为止,我们尚未讨论 MultiBinding(其目标属性具有绑定集合)。对于 MultiBinding,可以使用自定义 IMultiValueConverter 从绑定的值生成最终值。例如,可以从红色、蓝色和绿色的值来计算颜色,这些值可以是来自于相同或不同绑定源对象的值。

    绑定到集合

    绑定源对象可以视为其属性包含数据的单个对象,也可以视为通常组合在一起的多态对象的数据集合(如查询数据库的结果)。到目前为止,我们仅讨论了绑定到单个对象,但是绑定到数据集合是一个常见方案。例如,一个常见方案是使用 ItemsControl(如 ListBox、ListView 或 TreeView)来显示数据集合,如什么是数据绑定?一节中的应用程序所示。

    幸运的是,基本关系图仍然适用。如果要将 ItemsControl 绑定到集合,则关系图如下所示:

    正如此图中所示,若要将 ItemsControl 绑定到集合对象,应使用 ItemsSource 属性。可以将 ItemsSource 属性视为 ItemsControl 的内容。请注意,绑定是 OneWay,因为 ItemsSource 属性默认情况下支持 OneWay 绑定。

    如何实现集合
    可以枚举实现 IEnumerable 接口的任何集合。但是,若要设置动态绑定,以使集合中的插入或移除操作可以自动更新 UI,则该集合必须实现 INotifyCollectionChanged 接口。此接口公开一个事件,只要基础集合发生更改,都应该引发该事件。

    WPF 提供 ObservableCollection<(Of <(T>)>) 类,它是公开 INotifyCollectionChanged 接口的数据集合的内置实现。请注意,为了完全支持将数据值从源对象传送到目标,支持可绑定属性的集合中的每个对象还必须实现 INotifyPropertyChanged 接口。

    在实现自己的集合之前,请先考虑使用 ObservableCollection<(Of <(T>)>) 或一个现有的集合类,如 List<(Of <(T>)>)、Collection<(Of <(T>)>) 和 BindingList<(Of <(T>)>) 等。如果您有高级方案并且希望实现自己的集合,请考虑使用 IList,它提供可以按索引逐个访问的对象的非泛型集合,因而可提供最佳性能。

    集合视图
    一旦 ItemsControl 绑定到数据集合,您可能希望对数据进行排序、筛选或分组。若要执行此操作,可以使用集合视图,这是实现 ICollectionView 接口的类。

    什么是集合视图?
    可以将集合视图视为位于绑定源集合顶部的层,您可以通过它使用排序、筛选和分组查询来导航和显示源集合,所有这些操作都无需操作基础源集合本身。如果源集合实现了 INotifyCollectionChanged 接口,则 CollectionChanged 事件引发的更改将传播到视图。

    由于视图不会更改基础源集合,因此每个源集合可以有多个关联的视图。例如,您可以有 Task 对象的集合。通过使用视图,可以通过多种不同的方式来显示相同数据。例如,您可能希望在页面左侧显示按优先级排序的任务,而在页面右侧显示按区域分组的任务。

    如何创建视图
    创建和使用视图的一种方式是直接实例化视图对象并将其用作绑定源。例如,请考虑在什么是数据绑定?一节中演示的数据绑定演示应用程序。 该应用程序的实现方式是将 ListBox 绑定到数据集合上的视图,而不是直接绑定到数据集合。下面的示例摘自数据绑定演示应用程序。CollectionViewSource 类是 CollectionView 的可扩展应用程序标记语言 (XAML) 代理。在此特定示例中,视图的 Source 绑定到当前应用程序对象的 AuctionItems 集合(类型为 ObservableCollection<(Of <(T>)>))。

    资源 listingDataView 随后用作应用程序中元素(如 ListBox)的绑定源:

    若要为同一集合创建另一个视图,可以创建另一个 CollectionViewSource 实例并为其提供另一个 x:Key 名称。

    将视图用作绑定源并不是创建和使用集合视图的唯一方式。所有集合都具有一个默认集合视图。例如,对于所有实现 IEnumerable 的集合,CollectionView 都是默认视图对象。ListCollectionView 是实现 IList 的集合的默认视图对象,而 BindingListCollectionView 是用于那些实现 IBindingList 的集合的集合视图类。若要获取默认视图,请使用 GetDefaultView 方法。

    排序
    如前所述,视图可以将排序顺序应用于集合。如同在基础集合中一样,数据可能具有或不具有相关的固有顺序。通过集合上的视图,您可以根据您提供的比较条件来应用顺序或更改默认顺序。由于这是基于客户端的数据视图,因此一种常见情况是用户可能希望根据列对应的值,对多列表格数据进行排序。通过使用视图,可以应用这种用户驱动的排序,而无需对基础集合进行任何更改,甚至不必再次查询集合内容。

    下面的示例演示什么是数据绑定?一节中的应用程序 UI 的“Sort by category and date”(按类别和日期排序)CheckBox 的排序逻辑:

    筛选
    视图还可以将筛选器应用于集合。这意味着即使集合中可能存在一个项,此特定视图也仅用于显示整个集合的某个子集。可以根据条件在数据中进行筛选。例如,正如在什么是数据绑定? 一节中的应用程序的工作方式那样,“Show only bargains”(仅显示成交商品)CheckBox 包含了筛选出成交价为 25 美元或更高的项的逻辑。执行下面的代码可以在选中该 CheckBox 时将 ShowOnlyBargainsFilter 设置为 Filter 事件处理程序:

    ShowOnlyBargainsFilter 事件处理程序具有以下实现:

    如果直接使用一个 CollectionView 类而不是 CollectionViewSource,则应使用 Filter 属性来指定回调。

    分组
    视图支持分组功能,此功能使用户能够将集合视图中的集合分区为逻辑组。这些组可以是显式的,其中的用户提供组列表,也可以是隐式的,其中的组依据数据动态生成。

    下面的示例演示“Group by category”(按类别分组)CheckBox 的逻辑:

    当前记录指针
    视图还支持当前项的概念。可以在集合视图中的对象之间导航。在导航时,您是在移动记录指针,该指针可用于检索存在于集合中的特定位置的对象。

    请注意,当前记录指针的移动与应用于该集合的任何排序或筛选会相互产生某些影响。排序将当前记录指针保留在所选的最后一条记录上,但集合视图现在是围绕此指针重构的。(或许所选记录以前曾位于列表的开头,但现在所选记录可能在中间的某个位置。) 如果所选内容在筛选之后保留在视图中,则筛选操作会保留所选记录。否则,当前记录指针会设置到经过筛选的集合视图的第一条记录。

    主-从绑定方案
    当前项的概念不仅用于集合中项的导航,而且用于主-从绑定方案。再考虑一下什么是数据绑定?中的应用程序 UI。 在该应用程序中,ListBox 的所选内容决定了在 ContentControl 中显示的内容。换句话说,当选中一个 ListBox 项时,ContentControl 会显示选定项的详细信息。

    通过将两个或更多控件绑定到同一视图可以轻松地实现主-从方案。下面这个摘自数据绑定演示的示例演示什么是数据绑定?中的应用程序 UI 上看到的 ListBox 和 ContentControl 的标记:

    请注意,这两个控件都绑定到同一个源,即 listingDataView 静态资源(请参见如何创建视图一节中的此资源的定义)。可以这样做的原因是当单一实例对象(此示例中为 ContentControl)绑定到一个集合视图时,该对象会自动绑定到该视图的 CurrentItem。请注意,CollectionViewSource 对象会自动同步货币与所选内容。如果列表控件没有像示例中那样绑定到 CollectionViewSource 对象,则您需要将其 IsSynchronizedWithCurrentItem 属性设置为 true 以达到此目的。

    您可能已经注意到上面的示例使用了一个模板。实际上,如果不使用模板(由 ContentControl 显式使用的模板以及由 ListBox 隐式使用的模板),则数据不会按照我们希望的方式显示。现在,我们开始介绍下一节中的数据模板化。

    数据模板化
    如果不使用数据模板,则什么是数据绑定?一节中的应用程序 UI 将如下所示:

    如上一节中的示例所示,ListBox 控件和 ContentControl 都绑定到 AuctionItem 的整个集合对象(更具体地说,是绑定到集合对象上的视图)。如果没有关于如何显示数据集合的特定说明,则 ListBox 会显示基础集合中的每个对象的字符串表示形式,而 ContentControl 会显示绑定到的对象的字符串表示形式。

    若要解决该问题,应用程序应定义 DataTemplate。如上一节中的示例所示,ContentControl 显式使用 detailsProductListingTemplate DataTemplate。在显示集合中的 AuctionItem 对象时,ListBox 控件隐式使用下面的 DataTemplate:

      

    使用这两个 DataTemplate,生成的 UI 如什么是数据绑定?所示。 如屏幕快照所示,除了可以在控件中放置数据以外,使用 DataTemplate 还可以为数据定义引人注目的视觉效果。例如,上面的 DataTemplate 中使用了 DataTrigger,因而 SpecialFeatures 值为 HighLight 的 AuctionItem 会显示为带有橙色边框和一个星号。

    数据验证

    接受用户输入的大多数应用程序都需要具有验证逻辑,以确保用户输入了需要的信息。验证检查可以基于类型、范围、格式或其他应用程序特定的要求。本节讨论了数据验证在 WPF 中的工作方式。

    将验证规则与绑定关联
    使用 WPF 数据绑定模型可以将 ValidationRules 与 Binding 对象相关联。例如,下面是什么是数据绑定?一节中的 Add Product Listing(添加产品清单)“Start Price”(起始价格)TextBox 的 XAML:

    ValidationRules 属性采用 ValidationRule 对象的集合。ExceptionValidationRule 是内置的 ValidationRule,用于检查在绑定源属性的更新过程中引发的异常。在此特定示例中,绑定源属性为 StartPrice(属于整数类型),而目标属性为 TextBox.Text。当用户输入的值无法转换为整数时,将引发异常,这会导致将绑定标记为无效。

    也可以通过从 ValidationRule 类派生和实现 Validate 方法来创建自己的验证规则。下面的示例演示什么是数据绑定?一节中的 Add Product Listing(添加产品清单)“Start Date”(起始日期)TextBox 使用的规则:

    StartDateEntryForm TextBox 使用此 FutureDateRule,如下面的示例中所示:

    请注意,由于 UpdateSourceTrigger 值为 PropertyChanged,因此在每次键击时,绑定引擎都会更新源值,这意味着该引擎还会在每次键击时检查 ValidationRules 集合中的每条规则。我们会在“验证过程”一节中进行更深入的讨论。

    提供视觉反馈
    如果用户输入的值无效,则您可能希望在应用程序 UI 上提供一些有关错误的反馈。提供这种反馈的一种方式是将 Validation..::.ErrorTemplate 附加属性设置为自定义 ControlTemplate。如上一小节中所示,StartDateEntryForm TextBox 使用一个名为 validationTemplate 的 ErrorTemplate。下面的示例显示 validationTemplate 的定义:

    AdornedElementPlaceholder 元素指定要装饰的控件应放置的位置。

    此外,您可能还要使用 ToolTip 显示错误消息。StartDateEntryForm 和 StartPriceEntryForm TextBox 都使用样式 textStyleTextBox,该样式创建一个显示错误消息的 ToolTip。下面的示例显示 textStyleTextBox 的定义。当绑定元素的属性的一个或多个绑定发生错误时,附加属性 Validation.HasError 为 true。

    使用自定义 ErrorTemplate 和 ToolTip,StartDateEntryForm TextBox 在发生验证错误时会如下所示:

    如果您的 Binding 具有关联验证规则,但是您未在绑定控件上指定 ErrorTemplate,则在出现验证错误时会使用默认 ErrorTemplate 来通知用户。默认 ErrorTemplate 是一个在装饰器层中定义红色边框的控件模板。使用默认 ErrorTemplate 和 ToolTip,StartPriceEntryForm TextBox 的 UI 在发生验证错误时会如下所示:

     

    有关如何提供逻辑以验证对话框中的所有控件的示例,请参见对话框概述中的“自定义对话框”一节。

    验证过程
    由于数据验证与从目标到源的更新有关,因此它仅应用于 TwoWay 和 OneWayToSource 绑定。每次将输入值传输到绑定源属性时会发生验证。在这里重复一下,导致源更新的原因取决于 UpdateSourceTrigger 属性的值,如触发源更新的原因一节中所述。

    下面的插图提供了在数据绑定过程中适合进行验证的位置的可视表示形式:

    如上图所示,验证在调用转换器之前从目标到源的值传输过程中发生。下面将介绍验证 过程,如上面的关系图中所标记的那样:

    在将值从目标属性传输到源属性时,数据绑定引擎首先移除可能已添加到绑定元素的 Validation.Errors 附加属性的任何 ValidationError。然后,数据绑定引擎检查是否为该 Binding 定义了自定义 ValidationRule;如果已经定义,则它将调用每个 ValidationRule 上的 Validate 方法,直到其中一个规则出错或者全部规则都通过为止。

    如果某个自定义规则未通过,则绑定引擎会创建一个 ValidationError 对象,并将该对象添加到绑定元素的 Validation.Errors 集合。如果 Validation.Errors 不为空,则元素的 Validation.HasError 附加属性会设置为 true。此外,如果 Binding 的 NotifyOnValidationError 属性设置为 true,则绑定引擎将引发该元素上的 Validation.Error 附加事件。

    如果所有规则都通过,则绑定引擎会调用转换器(如果存在)。

    如果转换器通过,则绑定引擎会调用源属性的 setter。

    如果绑定具有与其关联的 ExceptionValidationRule,并且在步骤 4 中引发异常,则绑定引擎将检查是否存在 UpdateSourceExceptionFilter。您可以选择使用 UpdateSourceExceptionFilter 回调来提供用于处理异常的自定义处理程序。如果未对 Binding 指定 UpdateSourceExceptionFilter,则绑定引擎将对异常创建 ValidationError 并将其添加到绑定元素的 Validation.Errors 集合。

    还应注意,任何方向(目标到源或源到目标)的有效值传输操作都将清除 Validation.Errors 附加属性。

    调试机制
    可以在绑定相关对象上设置附加属性 PresentationTraceSources..::.TraceLevel 以接收有关特定绑定的状态的信息。

    1. WPF数据绑定之绑定源

    在数据绑定中,绑定源(源)对象指的是您从其获取数据的对象。本主题讨论可以用作源的对象类型。

    Windows Presentation Foundation (WPF) 数据绑定支持下列类型的绑定源:

      

    将 CLR 类用作绑定源对象
    本节讨论您在实现 CLR 类以用作源对象时需要了解的内容。

    提供更改通知
    如果您要使用 OneWay 或 TwoWay 绑定(您希望 UI 在源属性发生动态变化时进行更新),则必须实现一个适当的“属性已更改”通知机制。建议的机制是使 CLR 类实现 INotifyPropertyChanged 接口。

    如果您不实现 INotifyPropertyChanged,则必须安排您自己的通知系统,确保绑定中使用的数据是最新的。您可以通过支持需要更改通知的每个属性的 PropertyChanged 模式来提供更改通知。若要支持此模式,请定义每个属性的属性名称 Changed 事件,其中属性名称 是属性的名称。每次更改属性时都会引发此事件。

    如果源对象实现了正确的通知机制,则会自动进行目标更新。如果源对象由于某种原因未提供正确的属性更改通知,那么您可以选择使用 UpdateTarget 方法来显式更新目标属性。

    其他特征
    下表提供了需要注意的其他几个要点:

    如果您要在 XAML 中创建对象,则类必须使用默认构造函数。在某些 .NET 语言(例如 C#)中,可能已为您创建默认构造函数。

    用作绑定源属性的属性必须是类的公共属性。不能出于绑定目的来访问显式定义的接口属性,也不能访问没有基实现的受保护的私有或虚拟属性。

    您不能绑定到 CLR 类的公共字段。

    在您的类中声明的属性类型是传递到绑定的类型。但是,绑定最终使用的类型取决于绑定目标属性的类型,而不是源属性的类型。如果类型不同,您可能需要编写一个转换器来处理最初将您的自定义属性传递到绑定的方式。

    将整个对象用作绑定源
    可以将整个对象用作绑定源。要实现此目的,可以使用 Source 或 DataContext 属性将对象指定为绑定源,然后提供一个不含路径的空白绑定声明:{Binding}。这种方法可发挥较大作用的情况包括:要绑定到属于类型字符串的对象;要绑定到具有您关注的多个属性的对象;要绑定到集合对象。

    请注意,您可能需要应用自定义逻辑,使数据对您的绑定目标属性有意义。自定义逻辑的形式可以是自定义转换器(如果默认类型转换不存在)或者 DataTemplate。

    将集合对象用作绑定源
    通常,您要用作源的对象是包含多个自定义对象的集合,其中的每个自定义对象都代表一个数据对象,该数据对象用作重复绑定的一个实例的源。例如,您有一个 CustomerOrders 集合,它由 CustomerOrder 对象组成,您的应用程序可以循环访问该集合以确定存在多少订单以及每个订单包含的数据。

    可以枚举实现 IEnumerable 接口的任何集合。但是,若要设置动态绑定,以便集合中的插入或删除操作可以自动更新 UI,则该集合必须实现 INotifyCollectionChanged 接口。此接口公开一个事件,只要基础集合发生更改,就必须引发该事件。

    WPF 提供 ObservableCollection<(Of <(T>)>) 类,它是公开 INotifyCollectionChanged 接口的数据集合的内置实现。该集合中的各个数据对象必须满足前面各节中描述的要求。有关示例,请参见如何:创建和绑定到 ObservableCollection。在实现您自己的集合之前,请考虑使用 ObservableCollection<(Of <(T>)>) 或使用现有集合类之一,如 List<(Of <(T>)>)、Collection<(Of <(T>)>) 和 BindingList<(Of <(T>)>) 等。

    如果您有高级方案并且希望实现自己的集合,请考虑使用 IList,它提供可以按索引逐个访问的对象的非泛型集合,因而可提供最佳性能。

    权限要求
    下表总结了在完全信任或部分信任级别下执行的应用程序中可以绑定到的属性类型:

     
    下表描述了数据绑定中有关权限要求的要点:

    对于 CLR 属性,只要绑定引擎能够使用反射访问源属性,数据绑定就能进行。否则,绑定引擎会发出找不到属性的警告,并使用回退值或默认值(如果可用)。

    始终可以绑定到依赖项属性。

    XML 绑定的权限要求与此类似:在部分信任沙盒中,如果 XmlDataProvider 无权访问给定的数据,则它将失败。

    1. WPF数据绑定之数据模板

    WPF 数据模板模型为定义数据的表示提供了很大的灵活性。WPF 控件具有支持自定义数据表示的内置功能。本主题首先演示如何定义 DataTemplate,然后介绍其他数据模板功能,例如根据自定义逻辑选择模板和支持显示分层数据。

    先决条件
    本主题重点介绍数据模板功能,不介绍数据绑定概念。有关基本数据绑定概念的信息,请参见数据绑定概述。

    DataTemplate 用于数据表示,是 WPF 样式和模板模型提供的许多功能中的一种。

    另外,了解Resources也很重要,它实际上是有关使对象(例如,Style 和 DataTemplate)成为可重用对象的内容。

    数据模板基础

    为了说明 DataTemplate 为什么这么重要,让我们演示一个数据绑定的示例。在本示例中,有一个绑定到 Task 对象列表的 ListBox。每个 Task 对象都有 TaskName (string)、Description (string)、Priority (int) 和类型 TaskType 的属性,它是一个 Enum,其值为 Home 和 Work。

    没有 DataTemplate
    如果没有 DataTemplate,我们的 ListBox 当前具有如下外观:

     
    由于没有任何特定说明,ListBox 在尝试显示集合中的对象时,默认情况下会调用 ToString。因此,如果 Task 对象重写 ToString 方法,则 ListBox 显示基础集合中每个源对象的字符串表示形式。

    例如,如果 Task 类以这种方式重写 ToString 方法,其中 name 是 TaskName 属性的字段:

    则 ListBox 如下所示:

     
    但是,这是受到限制的,并不灵活。另外,如果您要绑定到 XML 数据,您不能重写 ToString。

    定义简单 DataTemplate
    该解决方案定义 DataTemplate。这样做的一种方式是将 ListBox 的 ItemTemplate 属性设置为 DataTemplate。DataTemplate 中指定的内容变成数据对象的可视结构。以下 DataTemplate 相当简单。我们要给出的说明是:每项显示为 StackPanel 中的三个 TextBlock 元素。每个 TextBlock 元素绑定到 Task 类的一个属性。

    本主题中的示例的基础数据是 CLR 对象的一个集合。如果您要绑定到 XML 数据,基本概念相同,但是语法稍微不同。例如,不是让 Path=TaskName,而是将 XPath 设置为 @TaskName(如果 TaskName 是 XML 节点的属性)。

    现在 ListBox 如下所示:

    将 DataTemplate 创建为资源
    在上面的示例中,我们定义了 DataTemplate 内联。更为常见的是在资源部分中定义它,以使其成为一个可重用的对象,如下面的示例所示:

    现在,您可以将 myTaskTemplate 用作资源,如下面的示例所示:

    因为 myTaskTemplate 是资源,所以您现在可以在具有采用 DataTemplate 类型的属性的其他控件中使用它。如上所述,对于 ItemsControl 对象(例如 ListBox),它是 ItemTemplate 属性。对于 ContentControl 对象,它是 ContentTemplate 属性。

    DataType 属性
    DataTemplate 类具有 DataType 属性,该属性非常类似于 Style 类的 TargetType 属性。因此,在上述示例中不需要为 DataTemplate 指定 x:Key,您可以执行以下操作:

    此 DataTemplate 自动应用于所有 Task 对象。请注意,在这种情况下,x:Key 是隐式设置的。因此,如果要为此 DataTemplate 分配一个 x:Key 值,则要重写隐式 x:Key 并且不会自动应用 DataTemplate。

    如果要将 ContentControl 绑定到 Task 对象的集合,则 ContentControl 不会自动使用以上 DataTemplate。这是因为在 ContentControl 上绑定需要更多的信息来区分是要绑定到整个集合还是要绑定到单个对象。如果 ContentControl 要跟踪对 ItemsControl 类型的选择,您可以将 ContentControl 绑定的 Path 属性设置为“/”以表示您对当前项感兴趣。有关示例,请参见如何:绑定到集合并基于选择显示信息。否则,需要通过设置 ContentTemplate 属性显式指定 DataTemplate。

    DataType 属性在您拥有不同类型数据对象的 CompositeCollection 时尤其有用。
    向 DataTemplate 添加更多信息
    当前,数据显示了必要的信息,但是还可以显示更多信息。让我们通过添加 Border、Grid 和一些用于描述要显示的数据的 TextBlock 来显示更多信息。

    下面的屏幕快照使用此已修改的 DataTemplate 来显示 ListBox。

    我们可以在 ListBox 上将 HorizontalContentAlignment 设置为 Stretch 来确保项的宽度占据整个空间:

    HorizontalContentAlignment 属性设置为 Stretch 后,ListBox 现在如下所示:

    将 DataTemplate 创建为资源
    在上面的示例中,我们定义了 DataTemplate 内联。更为常见的是在资源部分中定义它,以使其成为一个可重用的对象,如下面的示例所示:

    现在,您可以将 myTaskTemplate 用作资源,如下面的示例所示:

    因为 myTaskTemplate 是资源,所以您现在可以在具有采用 DataTemplate 类型的属性的其他控件中使用它。如上所述,对于 ItemsControl 对象(例如 ListBox),它是 ItemTemplate 属性。对于 ContentControl 对象,它是 ContentTemplate 属性。

    DataType 属性
    DataTemplate 类具有 DataType 属性,该属性非常类似于 Style 类的 TargetType 属性。因此,在上述示例中不需要为 DataTemplate 指定 x:Key,您可以执行以下操作:

    此 DataTemplate 自动应用于所有 Task 对象。请注意,在这种情况下,x:Key 是隐式设置的。因此,如果要为此 DataTemplate 分配一个 x:Key 值,则要重写隐式 x:Key 并且不会自动应用 DataTemplate。

    如果要将 ContentControl 绑定到 Task 对象的集合,则 ContentControl 不会自动使用以上 DataTemplate。这是因为在 ContentControl 上绑定需要更多的信息来区分是要绑定到整个集合还是要绑定到单个对象。如果 ContentControl 要跟踪对 ItemsControl 类型的选择,您可以将 ContentControl 绑定的 Path 属性设置为“/”以表示您对当前项感兴趣。有关示例,请参见如何:绑定到集合并基于选择显示信息。否则,需要通过设置 ContentTemplate 属性显式指定 DataTemplate。

    DataType 属性在您拥有不同类型数据对象的 CompositeCollection 时尤其有用。
    向 DataTemplate 添加更多信息
    当前,数据显示了必要的信息,但是还可以显示更多信息。让我们通过添加 Border、Grid 和一些用于描述要显示的数据的 TextBlock 来显示更多信息。

    下面的屏幕快照使用此已修改的 DataTemplate 来显示 ListBox。

    我们可以在 ListBox 上将 HorizontalContentAlignment 设置为 Stretch 来确保项的宽度占据整个空间:

    HorizontalContentAlignment 属性设置为 Stretch 后,ListBox 现在如下所示:

    DataTemplate 中有哪些内容?
    在前面的示例中,我们使用 DataTemplate.Triggers 属性将触发器放入 DataTemplate 中。触发器的 Setter 设置 DataTemplate 中元素(Border 元素)的属性值。但是,如果您的 Setters 相关属性不是当前 DataTemplate 中元素的属性,则使用 Style(用于 ListBoxItem 类)设置属性更合适(如果您要绑定的控件是 ListBox)。例如,如果您想要在鼠标指向某一项时让您的 Trigger 对该项的 Opacity 值进行动画处理,则需要在 ListBoxItem 样式中定义触发器。

    通常需要注意:DataTemplate 会应用于每个生成的 ListBoxItem(有关它实际应用的方式和场合的更多信息,请参见 ItemTemplate 页)。DataTemplate 仅与数据对象的表示和外观有关。在大多数情况下,所有其他表示方面(例如,项在被选中时的外观或 ListBox 排列项的方式)不在 DataTemplate 定义范围内。

    根据数据对象的属性选择 DataTemplate
    在 DataType 属性一节中,我们讨论了您可以针对不同的数据对象定义不同的数据模板。这在您拥有不同类型的 CompositeCollection 或不同类型的项集合时尤其有用。在使用 DataTrigger 来应用属性值一节中,我们演示了如果您拥有相同类型的数据对象集合,您可以创建 DataTemplate,然后根据每个数据对象的属性值使用触发器来应用更改。虽然触发器允许您应用属性值或启动动画,但是它们无法让您灵活重构数据对象的结构。在某些情况下,可能需要您为类型相同但属性不同的数据对象创建其他 DataTemplate。

    例如,当 Task 对象的 Priority 值为 1 时,您可能需要为它指定完全不同的外观,以给予您自己一个提醒。在这种情况下,您需要创建 DataTemplate 来显示高优先级的 Task 对象。让我们将以下 DataTemplate 添加到资源部分:

    请注意,此示例使用 DataTemplate.Resources 属性。DataTemplate 中的元素共享该部分中定义的资源。

    若要提供逻辑以根据数据对象的 Priority 值选择要使用的 DataTemplate,需要创建 DataTemplateSelector 的子类并重写 SelectTemplate 方法。在下面的示例中,SelectTemplate 方法提供逻辑以根据 Priority 属性的值返回适当的模板。可以在封装 Window 元素的资源中找到要返回的模板。

    然后,我们可以将 TaskListDataTemplateSelector 声明为资源:

    若要使用模板选择器资源,请将其分配到 ListBox 的 ItemTemplateSelector 属性。ListBox 为基础集合中的每一项调用 TaskListDataTemplateSelector 的 SelectTemplate 方法。该调用会将数据对象作为项参数来传递。然后,将由该方法返回的 DataTemplate 应用于该数据对象。

    使用模板选择器后,ListBox 现在如下所示:

     

    这正是此示例要得到的结果。

     对 ItemsControl 进行样式和模板处理
    即使 ItemsControl 不是 DataTemplate 所用于的唯一控件类型,将 ItemsControl 绑定到集合仍然很常见。在 DataTemplate 中有哪些内容一节中,我们讨论了您的 DataTemplate 定义应当仅与数据表示相关。为了明确何时不适合使用 DataTemplate,有必要了解 ItemsControl 提供的不同样式和模板属性。下面的示例旨在演示这些属性中每一个属性的功能。本示例中的 ItemsControl 绑定到与前面示例中的 Tasks 集合。为便于演示,本示例中的样式和模板都进行了内联声明。

    下面是该示例在呈现时的屏幕快照:

     
    请注意,您可以使用 ItemTemplateSelector,而不是 ItemTemplate。请参考上一节的示例。同样,可以选择使用 ItemContainerStyleSelector,而不是 ItemContainerStyle。

    这里未显示 ItemsControl 的其他两个与样式相关的属性,它们是 GroupStyle 和 GroupStyleSelector。

    对分层数据的支持
    到目前为止,我们仅讨论如何绑定和显示单个集合。某些时候,您要绑定的集合包含其他集合。HierarchicalDataTemplate 类专用于 HeaderedItemsControl 类型以显示这样的数据。在下面的示例中,ListLeagueList 是 League 对象的列表。每个 League 对象都有一个 Name 和 Division 对象的集合。每个 Division 都有一个 Name 和 Team 对象的集合,并且每个 Team 对象都有一个 Name。

    该示例演示通过使用 HierarchicalDataTemplate,您可以轻松地显示包含其他列表的列表数据。下面是该示例的一个屏幕快照。

    1. WPF数据绑定之绑定声明

    本主题讨论声明绑定的不同方法。

    先决条件
    在阅读本主题之前,应当先熟悉标记扩展的概念和使用,这一点非常重要。

    本主题不介绍数据绑定的概念。

    在 XAML 中声明绑定
    本节讨论如何在可扩展应用程序标记语言 (XAML) 中声明绑定。

    标记扩展使用
    Binding 是标记扩展。当您使用绑定扩展来声明绑定时,声明包含一系列子句,这些子句跟在 Binding 关键字后面,并由逗号 (,) 分隔。绑定声明中的子句可以按任意顺序排列,因此有许多可能的组合。子句是名称=值 对,其中名称 是 Binding 属性,值 是您要为该属性设置的值。

    当在标记中创建绑定声明字符串时,必须将它们附加到目标对象的特定依赖项属性。下面的示例演示如何通过使用绑定扩展并指定 Source、Path 和 UpdateSourceTrigger 属性来绑定 TextBox.Text 属性。

    您可以通过这种方法来指定 Binding 类的大部分属性。

    对象元素语法
    对象元素语法是创建绑定声明的另一种方法。在大多数情况下,使用标记扩展或对象元素语法没有特定的优势。但是,在标记扩展不支持您的方案的情况下,例如,当您的属性值是不存在类型转换的非字符串类型时,您将需要使用对象元素语法。

    下面是对象元素语法和标记扩展使用的一个示例:

    此示例通过使用扩展语法声明绑定来绑定 Foreground 属性。Text 属性的绑定声明使用对象元素语法。

    MultiBinding 和 PriorityBinding
    MultiBinding 和 PriorityBinding 不支持 XAML 扩展语法。因此,如果您要在 XAML 中声明 MultiBinding 或 PriorityBinding,则必须使用对象元素语法。

    在代码中创建绑定
    指定绑定的另一种方法是在代码中直接为 Binding 对象设置属性。下面的示例演示如何在代码中创建 Binding 对象并指定属性:

    如果您要绑定的对象是 FrameworkElement 或 FrameworkContentElement,则您可以直接对该对象调用 SetBinding 方法,而不是使用 BindingOperations..::.SetBinding。

    绑定路径语法
    使用 Path 属性可以指定您要绑定到的源值:

    在最简单的情况下,Path 属性值是要用于绑定的源对象的属性名,如 Path=PropertyName。

    在 C# 中可以通过类似语法指定属性的子属性。例如,子句 Path=ShoppingCart.Order 设置与对象或属性 ShoppingCart 的 Order 子属性的绑定。

    若要绑定到附加属性,应在附加属性周围放置圆括号。例如,若要绑定到附加属性 DockPanel..::.Dock,则语法是 Path=(DockPanel.Dock)。

    可以在要应用索引器的属性名后面的方括号内指定属性的索引器。例如,子句 Path=ShoppingCart[0] 将绑定设置为与属性的内部索引处理文本字符串“0”的方式对应的索引。还支持嵌套的索引器。

    可以在 Path 子句中混合索引器和子属性;例如,Path=ShoppingCart.ShippingInfo[MailingAddress,Street].

    在索引器内部,您可以有多个由逗号 (,) 分隔的索引器参数。可以使用圆括号指定每个参数的类型。例如,您可以有 Path="[(sys:Int32)42,(sys:Int32)24]",其中 sys 映射到 System 命名空间。

    转义机制
    在索引器 ([ ]) 内部,插入符号 (^) 用于对下一个字符进行转义。

    如果您在 XAML 中设置 Path,则还需要使用 XML 实体对 XML 分析程序专用的某些字符进行转义:

    使用 & 对字符“&”进行转义。

    使用 > 对结束标记“>”进行转义。

    此外,如果您使用标记扩展语法描述属性中的整个绑定,则需要使用反斜杠 \ 对 WPF 标记扩展分析程序专用的字符进行转义:

    反斜杠 \ 本身是转义字符。

    等号 (=) 将属性名与属性值隔开。

    逗号 (,) 用于分隔属性。

    右大括号 (}) 是标记扩展的结尾。

    默认行为
    如果未在声明中指定默认行为,则默认行为如下。

    创建一个尝试在绑定源值与绑定目标值之间执行类型转换的默认转换器。如果无法进行转换,则默认转换器会返回 null。

    如果您不设置 ConverterCulture,则绑定引擎会使用绑定目标对象的 Language 属性。在 XAML 中,此属性默认为“en-US”,或者从页面的根元素(或任何元素)继承值(如果已显式设置了一个值)。

    只要绑定已有数据上下文(例如,来自父元素的继承数据上下文),并且该上下文所返回的项或集合适合于绑定,而不需要进一步的路径修改,则绑定声明可以不必有任何子句:{Binding}。在绑定作用于集合的情况下,这通常是为数据样式指定绑定的方式。

    默认 Mode 可能是单向,也可能是双向,具体取决于所绑定的依赖项属性。您始终可以显式声明绑定模式,以确保绑定具有所需的行为。通常,用户可编辑的控件属性(如 TextBox..::.Text 和 RangeBase..::.Value)默认为双向绑定,而其他大多数属性默认为单向绑定。

    默认 UpdateSourceTrigger 值可能是 PropertyChanged,也可能是 LostFocus,具体也取决于所绑定的依赖项属性。多数依赖项属性的默认值都为 PropertyChanged,而 TextBox..::.Text 属性的默认值为 LostFocus。

    1. 实例一:绑定到ADO.NET数据源

    本示例演示如何将 Windows Presentation Foundation (WPF) ListBox 控件绑定到 ADO.NET DataSet。

    示例
    在本示例中,OleDbConnection 对象用于连接到数据源,该数据源是在连接字符串中指定的 Access MDB 文件。建立连接后,会创建一个 OleDbDataAdpater 对象。OleDbDataAdpater 对象执行一个 select 结构化查询语言 (SQL) 语句,以便从数据库中检索记录集。通过调用 OleDbDataAdapter 的 Fill 方法,此 SQL 命令的结果存储在 DataSet 的 DataTable 中。本示例中,DataTable 命名为 BookTable。然后,此示例将 ListBox 的 DataContext 属性设置为 DataSet 对象。

    然后,我们将 ListBox 的 ItemsSource 属性绑定到 DataSet 的 BookTable:

    BookItemTemplate 是定义数据显示形式的 DataTemplate:

    IntColorConverter 将 int 转换为颜色。利用此转换器,如果 NumPages 的值小于 350,则第三个 TextBlock 的 Background 颜色为绿色,否则为红色。此处未显示此转换器的实现。

    1. 实例二:绑定到LINQ查询的结果

    本示例演示如何运行 LINQ 查询然后绑定到查询结果。

    示例
    下面的示例创建两个列表框。第一个列表框包含三个列表项。

    在第一个列表框中选择一项会激发下面的事件处理程序。在本示例中,Tasks 是 Task 对象的集合。Task 类具有名为 Priority 的属性。此事件处理程序运行一个 LINQ 查询,该查询返回具有选定优先级值的 Task 对象的集合,然后将其设置为 DataContext:

    第二个列表框绑定到该集合,因为该列表框的 ItemsSource 值设置为 {Binding}。因此,该列表框显示返回的集合(基于 myTaskTemplate DataTemplate)。

    WPF图形和多媒体开发

    1. WPF 图形动画和媒体概述

    本主题介绍 Windows Presentation Foundation (WPF) 的图形、动画和媒体功能,使用这些功能,可以向应用程序添加图形、过渡效果、声音和视频。

    WPF 提供高级绘图和动画功能,以前只能从专用库(具体来说,就是 Microsoft Windows 图形设备接口 (GDI) 和 Microsoft Windows GDI+)使用这些功能。现在,WPF 提供对多媒体、向量图形、动画和内容撰写的集成支持,使得开发人员可以轻松地生成悦目的用户界面和内容。使用 Microsoft Visual Studio .NET 或者甚至文本编辑器(如 Microsoft 记事本),可以创建矢量图形或复杂的动画并将媒体集成到应用程序中。

    WPF 在图形和多媒体方面的新增功能
    WPF 向 Windows 开发人员引进了新的图形功能,这些功能具有如下优点:

    与分辨率和设备无关的图形。WPF 图形系统使用与设备无关的单元来支持分辨率和设备独立性。每个与设备无关的像素都会随系统上的每英寸点数设置自动缩放。

    更高的精度。WPF 坐标系使用双精度值,而不使用浮点值。转换值和不透明度值也是使用双精度值来表示。WPF 还支持更广泛的颜色域 (scRGB),并为管理来自不同颜色空间的输入提供了集成的支持。

    高级图形和动画支持。WPF 通过为您管理场景图简化了图形编程;您无需再担心场景处理、呈现循环和双线性内插算法。WPF 提供了命中测试支持、集成的动画系统和全面的字母合成支持。

    硬件加速。WPF 图形系统旨在利用图形硬件来最小化 CPU 使用率。

    二维形状
    WPF 提供了一个库,包含用矢量绘制的常用 二维 形状,如下图中演示的矩形和椭圆。


    这些内部的 WPF 形状不仅仅是形状:它们是可编程的元素,能够实现可通过最常见的控件(包括键盘输入和鼠标输入)实现的许多功能。

    下面的插图显示前面 XAML 标记和代码隐藏的输出。

    二维几何图形
    当 WPF 提供的 二维 形状不足时,可以使用 WPF 中的几何图形和路径支持来创建自己的形状。下面的插图显示如何使用几何图形来创建形状、如何将几何图形用作绘图画笔以及如何使用几何图形来剪裁其他 WPF 元素。

     

    二维效果
    WPF 提供了一个包含 二维 类的库,可用来创建各种效果。使用 WPF 的 二维 呈现功能,可以绘制具有渐变、位图、绘图和视频的 UI 元素,并借助于旋转、缩放和扭曲功能来操作这些元素。下图举例说明通过 WPF 画笔可获得的多种效果。

    三维呈现
    WPF 提供了一组三维呈现功能,这些功能可与 WPF 中的二维图形支持功能集成,以便您创建更加令人惊喜的布局、UI 和数据可视化效果。在色谱的一端,WPF 允许您将 二维 图像呈现到 三维 形状的一个图面上,如下图中所示。

    动画
    使用动画,可以使控件和元素变大、晃动、旋转和淡化,还可以产生有趣的页面过渡和更多效果。由于 WPF 允许您对大多数属性进行动画处理,因此,您不但可以对大多数 WPF 对象进行动画处理,而且还可以使用 WPF 来对您创建的自定义对象进行动画处理。

    媒体
    图像、视频和音频是用来传达信息和用户体验的富媒体方法。

    图像
    图像(包括图标、背景甚至动画的一部分)是大多数应用程序的核心部分。由于您经常需要使用图像,因此 WPF 提供了以各种方式处理图像的功能。下图只说明了其中的一种方法。

    视频和音频
    WPF 的一个核心图形功能就是为处理多媒体(包括视频和音频)提供本机支持。下面的示例说明如何在应用程序中插入媒体播放器。

    既能够播放视频又能够播放音频,而且具有很好的可扩展性,可以用来方便地创建自定义的 UI。

    1. WPF的图形呈现

    本主题概述 WPF 可视化层。本主题重点讲述 WPF 模型中呈现支持的 Visual 类的角色。

    Visual 对象的角色
    Visual 类是每个 FrameworkElement 对象所派生自的基本抽象。该类还充当在 WPF 中编写新控件的入口点。在 Win32 应用程序模型中,该类在许多方面可以被视为窗口句柄 (HWND)。

    Visual 对象是一个核心 WPF 对象,它的主要角色是提供呈现支持。用户界面控件(如 Button 和 TextBox)派生自 Visual 类,并使用该类来保持它们所呈现的数据。Visual 对象为下列功能提供支持:

    输出显示:呈现 Visual 对象的持久的序列化绘图内容。

    转换:针对 Visual 对象执行转换。

    剪辑:为 Visual 对象提供剪辑区域支持。

    命中测试:确定 Visual 对象的边界内是否包含坐标或几何形状。

    边界框计算:确定 Visual 对象的边框。

    但是,Visual 对象不包括对非呈现功能的支持,如:
    ◆事件处理

    ◆布局

    ◆样式

    ◆数据绑定

    ◆全球化

    Visual 作为子类必须派生自的公共抽象类进行公开。下图显示了 WPF 中所公开的可视化对象的层次结构。

    Visual 类的层次结构

     

    DrawingVisual
    DrawingVisual 是一个用于呈现形状、图像或文本的轻量绘图类。此类之所以被视为轻量,是因为它不提供布局或事件处理功能,从而能够改善运行时性能。因此,绘图最适于背景和剪贴画。DrawingVisual 可用于创建自定义可视化对象。

    Viewport3DVisual
    Viewport3DVisual 在二维 Visual 和 Visual3D 对象之间起到桥梁作用。Visual3D 类是所有三维可视化元素的基类。Viewport3DVisual 要求您定义一个 Camera 值和一个 Viewport 值。可以借助照相机来查看场景。投影映射到二维图面的区域称作视区。

    ContainerVisual
    ContainerVisual 类用作 Visual 对象集的容器。DrawingVisual 类派生自 ContainerVisual 类,这允许它包含可视化对象的集合。

    可视化对象中的绘图内容
    Visual 对象将它的呈现数据另存为向量图形指令列表。指令列表中的每一项都以序列化格式表示一组低级别的图形数据及其相关资源。共有四种不同类型的呈现数据可以包含绘图内容。

    通过 DrawingContext,您可用可视化内容填充 Visual。当您使用 DrawingContext 对象的绘图命令时,实际上是存储一组日后将由图形系统使用的呈现数据,而不是实时绘制到屏幕上。

    当您创建 WPF 控件(如 Button)时,该控件会为绘图对象本身隐式生成呈现数据。例如,设置 Button 的 Content 属性会导致该控件存储标志符号的呈现表示。

    Visual 将其内容描述为一个或多个包含在 DrawingGroup 中的 Drawing 对象。DrawingGroup 还描述不透明蒙板、转换、位图效果和应用于其内容的其他操作。呈现内容时,DrawingGroup 操作按如下顺序应用:OpacityMask、Opacity、BitmapEffect、ClipGeometry、GuidelineSet 和 Transform。

    下图显示了在呈现过程中 DrawingGroup 操作的应用顺序。

    在可视化层绘制内容
    绝不能直接实例化 DrawingContext;但可以通过某些方法(例如 DrawingGroup..::.Open 和 DrawingVisual..::.RenderOpen)获取绘图上下文。下面的示例从 DrawingVisual 中检索 DrawingContext 并将其用于绘制矩形。

    在可视化层枚举绘图内容
    此外,Drawing 对象还可提供用来枚举 Visual 内容的对象模型。

    说明:
    您在枚举可视化层的内容时,就是相当于在检索 Drawing 对象,而不是以向量图形指令列表形式检索呈现数据的基础表示。

    下面的示例使用 GetDrawing 方法来检索 Visual 的 DrawingGroup 值并枚举该值。

    如何使用可视化对象来生成控件
    WPF 中的许多对象都由其他可视化对象组成,这意味着它们可以包含子代对象的各种层次结构。WPF 中的许多用户界面元素(如控件)都由多个表示不同类型呈现元素的可视化对象组成。例如,Button 控件可以包含许多其他对象,其中包括 ClassicBorderDecorator、ContentPresenter 和 TextBlock。

    下面的代码显示的是在标记中定义的 Button 控件。

    如果您要枚举包含默认 Button 控件的可视化对象,则将发现如下所示的可视化对象层次结构:

    可视化树层次结构的关系图


    Button 控件包含一个 ClassicBorderDecorator 元素,该元素又包含一个 ContentPresenter 元素。ClassicBorderDecorator 元素负责为 Button 绘制边框和背景。ContentPresenter 元素负责显示 Button 的内容。在本例中,由于您要显示文本,因此 ContentPresenter 元素中包含一个 TextBlock 元素。Button 控件使用 ContentPresenter,这意味着该控件的内容可以由其他元素(如 Image)或几何形状(如 EllipseGeometry)来表示。

    控件模板
    将控件扩展为控件层次结构的关键在于 ControlTemplate。控件模板为控件指定默认的可视化层次结构。当您显式引用某个控件时,会隐式引用它的可视化层次结构。您可以重写控件模板的默认值,以便为控件创建自定义的可视化外观。例如,您可以修改 Button 控件的背景颜色值,以便它使用线性渐变颜色值,而不使用纯色值。

    用户界面元素(如 Button 控件)包含几个向量图形指令列表,这些列表描述控件的全部呈现定义。下面的代码显示的是在标记中定义的 Button 控件。

    如果您要枚举包含 Button 控件的可视化对象和向量图形指令列表,则将发现如下所示的可视化对象层次结构:

    可视化树和呈现数据的关系图


    Button 控件包含一个 ClassicBorderDecorator 元素,该元素又包含一个 ContentPresenter 元素。ClassicBorderDecorator 元素负责绘制所有构成按钮边框和背景的离散图形元素。ContentPresenter 元素负责显示 Button 的内容。在本例中,由于您要显示图像,因此 ContentPresenter 元素中包含一个 Image 元素。

    对于可视化对象和向量图形指令列表的层次结构,需要注意多个事项:

    该层次结构中的排序表示绘图信息的呈现顺序。从可视化元素的根,按照从左到右、从上到下的顺序遍历子元素。如果某个元素有可视化子元素,则会先遍历该元素的子元素,然后再遍历该元素的同级。

    层次结构中的非叶节点元素(如 ContentPresenter)用于包含子元素,它们并不包含指令列表。

    如果可视化元素既包含向量图形指令列表又包含可视化子级,则会先呈现父级可视化元素中的指令列表,然后再呈现任何可视化子对象中的绘图。

    向量图形指令列表中的项按照从左到右的顺序呈现。

    可视化树
    可视化树中包含某个应用程序的用户界面所使用的所有可视化元素。由于可视化元素中包含持久的绘图信息,因此您可以将可视化树视为场景图,其中包含将输出写入显示设备所必需的全部呈现信息。该树汇集了由该应用程序在代码或标记中直接创建的所有可视化元素。该可视化树还包含由元素(如控件和数据对象)的模板扩展功能创建的所有可视化元素。

    下面的代码显示的是在标记中定义的 StackPanel 元素。

    如果您要枚举包含标记示例中 StackPanel 元素的可视化对象,将发现如下所示可视化对象的层次结构:

    可视化树层次结构的关系图

     
     
    呈现顺序
    通过可视化树,可以确定 WPF 可视化对象和绘图对象的呈现顺序。将从位于可视化树中最顶层节点中的可视化元素根开始遍历,然后将按照从左到右的顺序遍历可视化元素根的子级。如果某个可视化元素有子级,则将先遍历该可视化元素的子级,然后再遍历其同级。这意味着子可视化元素的内容先于该可视化元素本身的内容而呈现。

    可视化树呈现顺序的关系图

     
     
    可视化元素根
    可视化元素根是可视化树层次结构中最顶层的元素。在大多数应用程序中,可视化元素根的基类是 Window 或 NavigationWindow。但是,如果您在 Win32 应用程序中承载可视化对象,则可视化元素根将是在 Win32 窗口中承载的最顶层的可视化元素。

    与逻辑树的关系
    WPF 中的逻辑树表示应用程序在运行时的元素。尽管您不直接操作该树,但是该应用程序视图对于了解属性继承和事件路由非常有用。与可视化树不同,逻辑树可以表示非可视化数据对象(如 ListItem)。在许多情况下,逻辑树密切映射到应用程序的标记定义。下面的代码显示的是在标记中定义的 DockPanel 元素。

    如果您要枚举包含标记示例中 DockPanel 元素的逻辑对象,则将发现如下所示逻辑对象的层次结构:

    逻辑树的关系图


    可视化树和逻辑树与当前的应用程序元素集合同步,并反映对元素进行的任何添加、删除或修改。但是,这些树表示不同的应用程序视图。与可视化树不同,逻辑树不展开控件的 ContentPresenter 元素。这意味着同一组对象的逻辑树和可视化树之间没有直接的一对一对应关系。实际上,在将同一个元素用作参数的情况下,调用 LogicalTreeHelper 对象的 GetChildren 方法与调用 VisualTreeHelper 对象的 GetChild 方法会生成不同的结果。

    使用 XamlPad 查看可视化树
    WPF 工具 (XamlPad) 提供了一个用来查看和浏览可视化树的选项,该树与当前所定义的 XAML 内容相对应。单击菜单栏上的“显示可视化树”[Show Visual Tree]按钮可显示相应的可视化树。下面将说明如何在 XamlPad 的“可视化树资源管理器”[Visual Tree Explorer]面板中将 XAML 内容扩展为可视化树节点:

    XamlPad 中的“可视化树资源管理器”[Visual Tree Explorer]面板

     
    请注意,Label、TextBox 和 Button 控件在 XamlPad 的“可视化树资源管理器”[Visual Tree Explorer]面板中各自显示一个可视化对象层次结构。这是由于 WPF 控件具有一个包含其可视化树的 ControlTemplate。当您显式引用某个控件时,会隐式引用它的可视化层次结构。

    分析可视化性能
    WPF 提供了一套性能分析工具,来帮助您分析应用程序的运行时行为,并确定可以应用的性能优化的类型。可视化探查器工具通过直接映射到应用程序的可视化树来为性能数据提供一个丰富的图形视图。在此屏幕快照中,可视化探查器的“CPU Usage”[CPU 使用率]部分使您可以清楚地了解对象对 WPF 服务(如呈现和布局)的使用情况。

    可视化探查器显示输出

    可视化对象的呈现行为
    WPF 引进了几个影响可视化对象呈现行为的功能:保留的模式图形、矢量图形和与设备无关的图形。

    保留的模式图形
    了解即时模式和保留模式图形系统之间的区别是了解 Visual 对象角色的要点之一。基于 GDI 或 GDI+ 的标准 Win32 应用程序使用即时模式图形系统。这意味着应用程序负责重新绘制工作区中由于某项操作(如调整窗口大小)或者对象的可视化外观发生变化而失效的部分。

    Win32 呈现顺序的关系图


    与之相比,WPF 使用保留模式系统。这意味着具有可视化外观的应用程序对象定义一组序列化绘图数据。在定义了绘图数据之后,系统会响应所有的重新绘制请求来呈现应用程序对象。甚至在运行时,您也可以修改或创建应用程序对象,并仍旧依赖系统响应绘制请求。保留模式图形系统中有一个强大功能,那就是绘图信息总是由应用程序保持为序列化状态,但是呈现功能仍由系统负责。下面的关系图演示应用程序如何依赖 WPF 来响应绘制请求。

    WPF 呈现顺序的关系图

     
    智能重绘
    使用保留模型图形的最大好处之一就是,WPF 可以高效率地优化需要在应用程序中重绘的内容。即使您有一个具有各种不透明度的复杂场景,通常也不必编写特殊用途的代码来优化重绘功能。请将智能重绘功能与 Win32 编程进行比较,在后者中,可以通过最小化更新区域中的重绘量来尽力优化应用程序。

    向量图形
    WPF 使用向量图形作为其呈现数据的格式。向量图形(包括可缩放的向量图形 (SVG)、Windows 元文件 (.wmf) 和 TrueType 字体)存储呈现数据,并以指令列表的形式传输该呈现数据,这些指令描述如何使用图形基元来重新创建图像。例如,TrueType 字体是描述一组直线、曲线和命令(而不是像素数组)的矢量字。矢量图形的主要好处之一就是能够伸缩到任何大小和分辨率。

    与矢量图形不同,位图图形以图像的逐像素表示形式来存储呈现数据,而且在特定的分辨率下预先呈现。位图图形格式和矢量图形格式的主要区别之一就是对原始图像的保真度。例如,当某个源图像的大小发生变化时,位图图形系统会拉伸该图像,而向量图形系统会伸缩该图像,从而保持图像的保真度。

    下图显示了源图像在放大到 3 倍时的情况。请注意,当源图像作为位图图形拉伸时会发生失真,而当源图像作为矢量图形伸缩时,则不会发生失真。

    光栅图形和矢量图形之间的区别


    下面的标记显示所定义的两个 Path 元素。第二个元素使用 ScaleTransform 将第一个元素的绘图指令放大到 3 倍。请注意 Path 元素中的绘图指令保持不变。

    关于分辨率和与设备无关的图形
    可通过以下两个系统因子来确定屏幕上的文本大小和图形大小:分辨率和 DPI。分辨率描述出现在屏幕上的像素数量。由于分辨率变大,因此像素会变小,从而导致所显示的图形和文本会变小。在将显示器的分辨率从 1024 x 768 更改为 1600 x 1200 时,显示器上所显示的图形会小得多。

    另一个系统设置 (DPI) 以像素数来描述屏幕英寸的大小。大多数 Windows 系统的 DPI 都为 96,这意味着一屏幕英寸等于 96 个像素。增加 DPI 设置会使屏幕英寸变大,减小 DPI 会使屏幕英寸变小。这意味着屏幕英寸与实际的英寸不相等;在多数系统上,二者很有可能不相等。当您增加 DPI 时,屏幕英寸会变大,因此支持 DPI 的图形和文本也会变大。增加 DPI 可能会增强文本的可读性,在高分辨率下尤其如此。

    并非所有的应用程序都支持 DPI:一些应用程序将硬件像素作为其主要计量单位;更改系统 DPI 不会对这些应用程序产生任何影响。许多其他应用程序都使用支持 DPI 的单位来描述字号,使用像素来描述任何其他内容。DPI 太小或太大都可能会导致这些应用程序出现布局问题,因为应用程序的文本会随着系统的 DPI 设置而伸缩,而应用程序的 UI 却不会出现此类问题。对于使用 WPF 开发的应用程序,此问题已经消除。

    WPF 支持通过将与设备无关的像素(而不是硬件像素)用作其主要计量单位来自动伸缩;图像和文本会适当伸缩,而无需应用程序开发人员执行任何额外的工作。下图显示了 WPF 文本和图形在不同 DPI 设置下的显示方式的示例。

    不同 DPI 设置下的图形和文本

     
    VisualTreeHelper
    VisualTreeHelper 类是一个静态帮助器类,它提供了一个要在可视化对象级别编程的低级功能,该类在非常特殊的方案(如开发高性能自定义控件)中非常有用。在大多数情况下,更高级的 WPF 框架对象(如 Canvas 和 TextBlock)提供更大的灵活性且更易于使用。

    命中测试
    VisualTreeHelper 类提供了当默认的命中测试支持无法满足您的需要时,针对可视化对象的命中测试方法。可以在 VisualTreeHelper 类中使用 HitTest 方法来确定几何形状或点坐标值是否位于给定对象(如控件或图形元素)的边界内。例如,您可以使用命中测试来确定鼠标在对象边框中的单击点是否落在圆形几何形状内部。您还可以选择重写对命中测试的默认实现来执行自己的自定义命中测试计算。

    枚举可视化树
    VisualTreeHelper 类提供了用来枚举可视化树成员的功能。若要检索父级,请调用 GetParent 方法。若要检索可视化对象的子级或直接子代,请调用 GetChild 方法。此方法返回父级在指定索引处的子 Visual。

    下面的示例演示如何枚举一个可视化对象的所有子代,如果您对序列化可视化对象层次结构的所有呈现信息感兴趣,则可能希望使用该技术。

    在大多数情况下,逻辑树更能表示 WPF 应用程序中的元素。尽管您不直接修改逻辑树,但是该应用程序视图对于了解属性继承和事件路由非常有用。与可视化树不同,逻辑树可以表示非可视化数据对象(如 ListItem)。
    VisualTreeHelper 类提供用来返回可视化对象边框的方法。可以通过调用 GetContentBounds 来返回可视化对象的边框。可以通过调用 GetDescendantBounds 来返回可视化对象及其所有子代的边框。下面的代码演示如何计算可视化对象及其所有子代的边框。

    1. WPF的图像处理

    本主题介绍 WPF 图像处理。通过 Microsoft Windows Presentation Foundation 图像处理组件,开发人员可以显示、转换和格式化图像。

    WPF 图像处理组件
    WPF 图像处理使得 Microsoft Windows 内的图像处理功能得到了极大改进。以前,图像处理功能(如显示位图或在常见控件上使用图像)依赖于 Microsoft Windows 图形设备接口 (GDI) 或 Microsoft Windows GDI+ 库。这些 API 提供有基本图像处理功能,但缺少诸如支持编解码器扩展性和高保真图像支持等功能。WPF 图像处理旨在克服 GDI 和 GDI+ 的缺点,并提供一组新的 API,用以在应用程序内显示和使用图像。

    有两种方式可以访问 WPF 图像处理 API:托管组件和非托管组件。非托管组件提供以下功能。

    适用于新的或专用图像格式的扩展性模型。

    对包括位图 (BMP)、联合图像专家组 (JPEG)、可移植网络图形 (PNG)、标记图像文件格式 (TIFF)、Microsoft Windows Media 照片、图形交换格式 (GIF) 和图标 (.ico) 在内的本机图像格式增强了性能和安全性。

    高位深图像数据的保留最多 32 位/通道。

    非破坏性图像缩放、裁切和旋转。

    简化的颜色管理

    支持文件内的专用元数据。

    托管组件利用非托管基础结构提供图像与其他 WPF 功能(如用户界面 (UI)、动画和图形)的无缝集成。托管组件还可以从 Windows Presentation Foundation (WPF) 图像处理编解码器扩展性模型获益,利用该模型可以实现自动识别 WPF 中的新图像格式。

    大部分托管的 WPF 图像处理 API 驻留在 System.Windows.Media.Imaging 命名空间中,不过,几个重要的类型(如 ImageBrush 和 ImageDrawing)都驻留在 System.Windows.Media 命名空间,Image 驻留在 System.Windows.Controls 命名空间。

    本主题提供有关托管组件的其他信息。

    WPF 图像格式
    编解码器用于对特定媒体格式解码或编码。WPF 图像处理包括一个适用于 BMP、JPEG、PNG、TIFF、Windows Media 照片、GIF 和 ICON 图像格式的编解码器。利用上述每个编解码器,应用程序可以对其各自的图像格式进行解码(ICON 除外)和编码。

    BitmapSource 是一个重要的类,用于对图像进行解码和编码。它是 WPF 图像处理管线的基本构造块,表示具有特定大小和分辨率的单个不变的像素集。BitmapSource 可以是多个帧图像中的单个帧,或者可以是在 BitmapSource 上执行转换的结果。它是 WPF 图像处理中使用的许多主要类(如 BitmapFrame)的父级。

    BitmapFrame 用于存储图像格式的实际位图数据。许多图像格式仅支持单一 BitmapFrame,不过 GIF 和 TIFF 等格式的图像支持每个图像有多个帧。帧由解码器用作输入数据,并传递到编码器以创建图像文件。

    下面的示例演示如何从 BitmapSource 创建一个 BitmapFrame 并将其添加到 TIFF 图像。

    图像格式解码
    图像解码是指将某种图像格式转换为可以由系统使用的图像数据。然后,此图像数据可以用于显示、处理或编码为其他格式。解码器的选择是基于图像格式做出的。编解码器的选择是自动做出的,除非指定了特定的解码器。在 WPF 中显示图像小节中的示例演示了自动解码。使用非托管 WPF 图像处理界面开发并向系统注册的自定义格式解码器会自动加入到解码器选择队列。这将使得自定义格式可以自动显示在 WPF 应用程序中。

    下面的示例演示使用位图解码器对 BMP 格式的图像进行解码。

    图像格式编码
    图像编码是指将图像数据转换为特定图像格式的过程。然后,已编码的图像数据可以用于创建新图像文件。WPF 图像处理为上面介绍的每种图像格式提供编码器。

    下面的示例演示使用编码器保存一个新创建的位图图像。

    在 WPF 中显示图像
    可以通过多种方式在 Windows Presentation Foundation (WPF) 应用程序中显示图像。可以使用 Image 控件显示图像、使用 ImageBrush 在可视图面上绘制图像或使用 ImageDrawing 绘制图像。

    使用 Image 控件
    Image 是一个框架元素,是在应用程序中显示图像的主要方式。在 XAML 中,有两种方式可以使用 Image:属性 (attribute) 语法或属性 (property) 语法。下面的示例演示如何使用属性 (attribute) 语法或属性 (property) 标记语法来呈现一个宽 200 像素的图像。

    许多示例使用 BitmapImage 对象引用图像文件。BitmapImage 是一个专用的 BitmapSource,已经优化为用于可扩展应用程序标记语言 (XAML) 加载,是一种将图像显示为 Image 控件的 Source 的简便方式。

    下面的示例演示如何使用代码呈现宽 200 像素的图像。

    说明:
    BitmapImage 实现 ISupportInitialize 接口,以对多个属性的初始化进行优化。只能在对象初始化过程中进行属性更改。调用 BeginInit 以表示初始化开始;调用 EndInit 以表示初始化结束。初始化一旦开始之后,将忽略所做的属性更改。

    旋转、转换和裁切图像
    通过 WPF,用户可以使用 BitmapImage 的属性或使用其他 BitmapSource 对象(如 CroppedBitmap 或 FormatConvertedBitmap)来转换图像。上述图像转换可以缩放或旋转图像、更改图像的像素格式或裁切图像。

    可以使用 BitmapImage 的 Rotation 属性来执行图像旋转。旋转只能以 90 度的增量来进行。在下面的示例中,图像旋转了 90 度。

    可以使用 FormatConvertedBitmap 将图像转换为不同的像素格式,如灰度。在下面的示例中,图像转换为 Gray4。

    旋转、转换和裁切图像
    通过 WPF,用户可以使用 BitmapImage 的属性或使用其他 BitmapSource 对象(如 CroppedBitmap 或 FormatConvertedBitmap)来转换图像。上述图像转换可以缩放或旋转图像、更改图像的像素格式或裁切图像。

    可以使用 BitmapImage 的 Rotation 属性来执行图像旋转。旋转只能以 90 度的增量来进行。在下面的示例中,图像旋转了 90 度。

    可以使用 FormatConvertedBitmap 将图像转换为不同的像素格式,如灰度。在下面的示例中,图像转换为 Gray4。

    若要裁切图像,可以使用 Image 或 CroppedBitmap 的 Clip 属性。通常情况下,如果您只想调整图像的一部分,则应使用 Clip。如果需要编码和保存裁切过的图像,应使用 CroppedBitmap。下面的示例使用 EllipseGeometry 和 Clip 属性来裁切图像。

    拉伸图像
    Stretch 属性控制如何拉伸一个图像以使其填充容器。Stretch 属性接受以下值(是由 Stretch 枚举定义的):

    None:不会拉伸图像以填充输出区域。如果图像比输出区域大,则图像将绘制到输出区域,而无法容纳的内容将被剪裁掉。

    Fill:会拉伸图像以适应输出区域。由于图像的高度和宽度是独立进行缩放的,因此图像的原始长宽比可能不会保留。也就是说,为了完全填充输出容器,图像可能会扭曲。

    Uniform:图像进行缩放,以便其完全适应输出区域。图像的长宽比会保留。

    UniformToFill:图像会进行缩放,以便在保留图像原始长宽比的同时完全填充输出区域。

    下面的示例将每个可用的 Stretch 枚举应用于 Image。

    下面的图像显示示例的输出,演示了不同的 Stretch 设置在应用到图像时的效果。

    不同的拉伸设置

    绘制图像
    您还可以利用 Brush 进行绘制,从而在应用程序中显示图像。利用画笔,您可以利用任意内容(从简单的纯色到复杂的图案和图像集)绘制 UI 对象。若要绘制图像,请使用 ImageBrush。ImageBrush 是一种 TileBrush 类型,用于将其内容定义为位图图像。ImageBrush 显示由其 ImageSource 属性指定的单个图像。您可以控制图像的拉伸、对齐和平铺方式,从而可以防止失真并生成图案和其他效果。下图显示了利用 ImageBrush 可以达到的几种效果。

    图像的画笔可以填充形状、控件、文本等


    下面的示例演示如何使用 ImageBrush 绘制带图像的按钮的背景。

    图像元数据
    某些图像文件包含用于描述文件的内容或特征的元数据。例如,大多数数码相机创建的图像中包含用于捕获该图像的照相机的品牌和型号的元数据。每个图像格式处理元数据的方式不同,但 WPF 图像处理提供一种统一的方式来为每个支持的图像格式存储和检索元数据。

    对元数据的访问是通过 BitmapSource 对象的 Metadata 属性来进行的。Metadata 返回一个 BitmapMetadata 对象,其中包含该图像所包含的所有元数据。此数据可以位于一个元数据架构中或位于不同方案的组合中。WPF 图像处理 支持以下图像元数据架构:可交换图像文件 (Exif)、tEXt(PNG 文本数据)、图像文件目录 (IFD)、国际新闻通信委员会 (IPTC) 和可扩展元数据平台 (XMP)。

    为了简化读取元数据的过程,BitmapMetadata 提供易于访问的若干命名属性(如 Author、Title 和 CameraModel)。许多命名属性还可以用于编写元数据。元数据查询读取器提供对读取元数据的其他支持。GetQuery 方法用于检索元数据查询读取器,这是通过提供字符串查询(如 "/app1/exif/")来实现的。在下面的示例中,GetQuery 用于获取存储在 "/Text/Description" 位置中的文本。

    若要编写元数据,会使用到元数据查询编写器。SetQuery 获取查询编写器并设置需要的值。在下面的示例中,SetQuery 用于编写存储在 "/Text/Description" 位置中的文本。

    编解码器扩展性
    WPF 图像处理的核心功能是用于新图像编解码器的扩展性模型。通过这些非托管的接口,编解码器开发人员可以将编解码器与 WPF 集成,这样 WPF 应用程序就可以自动使用新的图像格式。

    说明:
    编解码器必须进行数字签名,以便于系统识别。

    1. WPF的三维图形应用

    本主题概述 Windows Presentation Foundation (WPF) 图形系统中的三维功能。通过 WPF 三维实现,开发人员可使用与该平台所提供给二维图形的相同的功能,对标记和过程代码中的三维图形进行绘制、转换和动画处理。 开发人员可以合并二维和三维图形来创建丰富的控件,提供复杂的数据图解,或者增强用户对应用程序界面的体验。WPF 支持三维的设计宗旨不是提供功能齐全的游戏开发平台。

    二维容器中的三维
    WPF 中的三维图形内容封装在 Viewport3D 元素中,该元素可以参与二维元素结构。与 WPF 中的许多其他内容一样,图形系统将 Viewport3D 视为二维可视化元素。Viewport3D 充当三维场景中的一个窗口(视区)。更准确地说,它是三维场景所投影到的图面。

    在传统的二维应用程序中,当您需要使用 Grid 或 Canvas 之类的另一个容器元素时,可以使用 Viewport3D。 尽管您可以将 Viewport3D 与同一个场景图中的其他二维绘图对象结合使用,但是您不能在 Viewport3D 内部渗透二维和三维对象。 本主题重点讲述如何在 Viewport3D 内部绘制三维图形。

    三维坐标空间
    二维图形的 WPF 坐标系将原点定位在呈现区域(通常是屏幕)的左上角。在二维系统中,x 轴上的正值朝右,y 轴上的正值朝下。 但是,在三维坐标系中,原点位于呈现区域的中心,x 轴上的正值朝右,但是 y 轴上的正值朝上,z 轴上的正值从原点向外朝向观察者。

    传统的二维和三维坐标系表示形式

      
    由这些轴定义的空间是三维对象在 WPF 中的固定参考框架。当您在该空间中生成模型并创建光源和照相机以查看这些模型时,一定要在向每个模型应用变换时,将固定参考框架或“全局空间”与您为该模型创建的局部参考框架区分开。另请记住,根据光源和照相机设置,全局空间中的对象可能会看上去完全不同或者根本不可见,但是照相机的位置不会改变对象在全局空间中的位置。

    照相机和投影
    处理二维对象的开发人员习惯于将绘图基元置于二维屏幕上。当您创建三维场景时,一定要记住您实际上是要创建三维对象的二维表示形式。由于三维场景的外观会因观察者的观察位置不同而异,因此您必须指定观察位置。可以使用 Camera 类来为三维场景指定观察位置。

    了解三维场景如何在二维图面上表示的另一种方法就是将场景描述为到观察表面上的投影。使用 ProjectionCamera,可以指定不同的投影及其属性以更改观察者查看三维模型的方式。PerspectiveCamera 指定用来对场景进行透视收缩的投影。 换言之,PerspectiveCamera 提供消失点透视。 您可以指定照相机在场景坐标系中的位置、照相机的方向和视野以及用来定义场景中“向上”方向的向量。下图阐释 PerspectiveCamera 的投影。

    ProjectionCamera 的 NearPlaneDistance 和 FarPlaneDistance 属性限制照相机的投影范围。由于照相机可以位于场景中的任何位置,因此照相机实际上可能会位于模型内部或者紧靠模型,这使得很难正确区分对象。使用 NearPlaneDistance,可以指定一个距离照相机的最小距离,即,在超过该距离后将不绘制对象。 相反,使用 FarPlaneDistance,可以指定一个距离照相机的距离(即,在超过该距离后将不绘制对象),从而确保因距离太远而无法识别的对象将不包括在场景中。

    照相机位置

     
    OrthographicCamera 指定三维模型到二维可视化图面上的正投影。与其他照相机一样,它指定位置、观察方向和“向上”方向。但是,与 PerspectiveCamera 不同的是,OrthographicCamera 描述了不包括透视收缩的投影。换言之,OrthographicCamera 描述了一个侧面平行的取景框,而不是侧面汇集在场景中一点的取景框。下图演示使用 PerspectiveCamera 和 OrthographicCamera 查看同一模型时的情况。

    透视投影和正投影

     
    下面的代码演示一些典型的照相机设置。

    模型和网格基元
    Model3D 是表示泛型三维对象的抽象基类。 若要生成三维场景,需要一些要查看的对象,而且构成场景图的对象必须派生自 Model3D。 目前,WPF 支持用 GeometryModel3D 对几何形状进行建模。此模型的 Geometry 属性采用网格基元。

    若要生成模型,请首先生成一个基元或网格。 三维基元是一系列构成单个三维实体的顶点。 大多数三维系统都提供在最简单的闭合图(由三个顶点定义的三角形)上建模的基元。 由于三角形的三个点在一个平面上,因此您可以继续添加三角形,以便对网格这样较为复杂的形状建模。

    WPF 三维系统目前提供 MeshGeometry3D 类,使用该类,可以指定任何几何形状;它目前不支持预定义的三维基元(如球体和立方体)。首先通过将三角形顶点的列表指定为它的 Positions 属性来创建 MeshGeometry3D。每个顶点都指定为 Point3D。 (在可扩展应用程序标记语言 (XAML) 中,将该属性指定为三个一组的数字列表,每组中的三个数字表示每个顶点的坐标)。 根据网格的几何形状,网格可能会由多个三角形组成,其中的一些三角形共用相同的角(顶点)。 若要正确地绘制网格,WPF 需要有关哪些顶点由哪些三角形共用的信息。 可以通过指定具有 TriangleIndices 属性的三角形索引列表来提供此信息。此列表指定在 Positions 列表中指定的点将按哪种顺序确定三角形。

    在上面的示例中,Positions 列表指定用八个顶点来定义立方体形状的网格。 TriangleIndices 属性指定了一个包含十二个组的列表,每组由三个索引组成。 列表中的每个数字都指向与 Positions 列表的偏移量。 例如,由 Positions 列表指定的第一组(三个顶点)是 (1,1,0)、(0,1,0) 和 (0,0,0)。由 TriangleIndices 列表指定的第一组(三个索引)是 0、2 和 1,这与 Positions 列表中的第一个、第三个和第二个点相对应。 因此,构成立方体模型的第一个三角形将按照从 (1,1,0) 到 (0,1,0) 再到 (0,0,0) 的顺序组合而成,其余的十一个三角形将按照类似方式确定。

    您可以通过为 Normals 和 TextureCoordinates 属性指定值来继续定义模型。 为了呈现模型的图面,图形系统需要有关曲面在任何给定三角形上的朝向信息。图形系统使用此信息来针对该模型进行照明计算:直接朝向光源的图面比偏离光源的图面显得更亮。尽管 WPF 可以使用位置坐标来确定默认的法向量,但是您还可以指定不同的法向量来近似计算曲面的外观。

    TextureCoordinates 属性指定 Point 集合,该集合可通知图形系统如何将用来确定纹理绘制方式的坐标映射到网格的顶点。TextureCoordinates 可指定为 0 和 1(包括 0 和 1)之间的值。 如同 Normals 属性一样,图形系统可以计算默认纹理坐标,但是您可以选择设置不同的纹理坐标来控制对包括重复图案一部分的纹理的映射。

    下面的示例演示如何在过程代码中创建立方体模型的一面。请注意,您可以将整个立方体绘制为单个 GeometryModel3D;此示例将该立方体的各个面绘制为一个不同的模型,以便在以后向每个面应用不同的纹理。

    向模型应用 Material
    为了使网格看上去像三维对象,必须向其应用纹理,以便覆盖由顶点和三角形定义的图面,从而使其可以由照相机照明和投影。在二维中,可以使用 Brush 类来向屏幕中的区域应用颜色、图案、渐变或其他可视化内容。 但是,三维对象的外观是照明模型的功能,而不只是应用于它们的颜色或图案。实际对象的图面质量不同,它们反射光的方式也会有所不同:光亮的图面与粗糙或不光滑的图面看上去不同,某些对象似乎可以吸收光,而某些对象似乎能够发光。您可以向三维对象应用与应用于二维对象的完全相同的画笔,但是您不能直接应用它们。

    WPF 使用 Material 抽象类来定义模型图面的特征。Material 的具体子类用来确定模型图面的某些外观特征,每个子类还提供一个可以向其传递 SolidColorBrush、TileBrush 或 VisualBrush 的 Brush 属性。

    DiffuseMaterial 指定将向模型应用画笔,就好像模型是使用漫射光来照亮的一样。使用 DiffuseMaterial 与直接针对二维模型使用画笔非常相似;模型表面不反射光,就好像是自发光一样。

    SpecularMaterial 指定将向模型应用画笔,就好像模型的表面坚硬或者光亮,能够反射光线一样。可以通过为 SpecularPower 属性指定一个值来设置系统将为纹理的反射特质(或“发光”)建议的度数。

    使用 EmissiveMaterial 可以指定将应用纹理,就好像模型所发出的光与画笔的颜色相同。这不会使模型成为光源;但是,它参与阴影设置的方式将不同于用 DiffuseMaterial 或 SpecularMaterial 设置纹理时的情况。

    为进一步提高性能,可以从场景中精选 GeometryModel3D 的背面(由于它们相对于照相机位于模型的背面,因此您将看不到这些面)。 若要指定要应用于模型(如飞机)背面的 Material,请设置模型的 BackMaterial 属性。

    为了实现某些图面质量(如发光或发射效果),您可能希望向模型连续应用几个不同的画笔。您可以使用 MaterialGroup 类来应用和重用多个 Material。MaterialGroup 的子级在多个呈现过程中按照从头到尾的顺序来应用。

    下面的代码示例演示如何将纯色和绘图以画笔形式应用于三维模型。

    照亮场景
    与实际的光一样,三维图形中的光能够使图面可见。更确切地说,光确定了场景的哪个部分将包括在投影中。WPF 中的光对象创建了各种光和阴影效果,而且是按照各种实际光的行为建模的。您必须至少在场景中包括一个光,否则模型将不可见。

    下面的光派生自基类 Light:

    AmbientLight:它所提供的环境光以一致的方式照亮所有的对象,而与对象的位置或方向无关。

    DirectionalLight:像远处的光源那样照亮。 将方向光的 Direction 指定为 Vector3D,但是没有为方向光指定位置。

    PointLight:像近处的光源那样照亮。PointLight 具有一个位置并从该位置投射光。场景中的对象是根据对象相对于光源的位置和距离而照亮的。PointLightBase 公开了一个 Range 属性,该属性确定一个距离,在超过该距离后模型将无法由光源照亮。PointLight 还公开了多个衰减属性,这些属性确定光源的亮度如何随距离的增加而减小。您可以为光源的衰减指定恒定、线性或二次内插算法。

    SpotLight:继承自 PointLight。Spotlight 的照亮方式与 PointLight 类似,但是它既具有位置又具有方向。它们在 InnerConeAngle 和 OuterConeAngle 属性所设置的锥形区域(以度为单位指定)中投射光。

    光源是 Model3D 对象,因此您可以转换光源对象并对光源属性(包括位置、颜色、方向和范围)进行动画处理。

    变换模型
    当您创建模型时,它们在场景中具有特定的位置。为了在场景中移动、旋转这些模型或者更改这些模型的大小而更改用来定义模型本身的顶点是不切实际的。 相反,正如在二维中一样,您可以向模型应用转换。

    每个模型对象都有一个可用来对模型进行移动、重定向或调整大小的 Transform 属性。 当您应用转换时,实际上是按照由转换功能指定的向量或值(以适用者为准)来有效地偏移模型的所有点。换言之,您已经变换了在其中定义模型的坐标空间(“模型空间”),但是,您尚未更改在整个场景的坐标系(“全局空间”)中构成模型几何形状的值。

    对模型进行动画处理
    WPF 三维实现与二维图形参与同一个计时和动画系统。换言之,若要对三维场景进行动画处理,需要对其模型的属性进行动画处理。 可以直接对基元的属性进行动画处理,但是通常很容易对用来更改模型位置或外观的变换进行动画处理。 由于可以向 Model3DGroup 对象及其各个模型应用转换,因此可以向 Model3DGroup 的子级应用一组动画,向一组子对象应用另一组动画。 还可以通过对场景的照明属性进行动画处理来实现各种可视化效果。 最后,您可以选择通过对照相机的位置或视野进行动画处理来对投影本身进行动画处理。

    若要对 WPF 中的对象进行动画处理,可以创建时间线、定义动画(实际上是随着时间的推移而更改某个属性值)并指定要向其应用动画的属性。 由于三维场景中的所有对象都是 Viewport3D 的子级,因此要应用于场景的任何动画所面向的属性都是 Viewport3D 的属性。

    假设您希望实现模型看上去是在原地摇摆的效果, 您可以选择向模型应用 RotateTransform3D,并对模型从一个向量旋转到另一个向量时所围绕的轴进行动画处理。下面的代码示例演示如何将 Vector3DAnimation 应用于该转换的 Rotation3D 的 Axis 属性,并假设 RotateTransform3D 是应用于具有 TransformGroup 的模型的几个转换之一。

    向窗口中添加三维内容
    若要呈现场景,请向 Model3DGroup 中添加模型和光源,然后将 Model3DGroup 设置为 ModelVisual3D 的 Content。将 ModelVisual3D 添加到 Viewport3D 的 Children 集合中。 通过设置 Viewport3D 的 Camera 属性来向其添加照相机。

    最后,请向该窗口中添加 Viewport3D。在将 Viewport3D 作为布局元素(如 Canvas)的内容来包含时,可以通过设置 Viewport3D 的 Height 和 Width 属性(继承自 FrameworkElement)来指定 Viewport3D 的大小。

    1. WPF的三维变换应用

    本主题描述如何向 Windows Presentation Foundation (WPF) 图形系统中的三维模型应用变换。开发人员可以借助于变换功能来对模型进行重定位、调整大小和重定向,而无需更改用来定义模型的基值。

    三维坐标空间
    Windows Presentation Foundation (WPF) 中的三维图形内容封装在 Viewport3D 元素中,该元素可以参与二维元素结构。该图形系统将 Viewport3D 视为一个像 Windows Presentation Foundation (WPF) 中的许多其他元素一样的二维可视化元素。Viewport3D 充当三维场景中的一个窗口(视区)。更准确地说,它是三维场景所投影到的图面。 尽管可以将 Viewport3D 与其他二维绘图对象用在同一个场景关系图中,但是不能在 Viewport3D 中渗透二维和三维对象。以下讨论中所描述的坐标空间包含在 Viewport3D 元素中。

    二维图形的 Windows Presentation Foundation (WPF) 坐标系将原点定位在呈现图面(通常是屏幕)的左上角。在二维系统中,x 轴上的正值朝右增加,y 轴上的正值朝下增加。但是,在三维坐标系中,原点位于屏幕的中心,x 轴上的正值朝右增加,但是 y 轴上的正值朝上增加,z 轴上的正值从原点向外朝向观察者增加。

    坐标系比较


    由这些轴定义的空间是三维对象在 Windows Presentation Foundation (WPF) 中的固定参考框架。当您在该空间中生成模型并创建光源和照相机以查看这些模型时,一定要在向每个模型应用变换时,将固定参考框架或“全局空间”与您为该模型创建的局部参考框架区分开。另请记住,根据光源和照相机设置,全局空间中的对象可能会看上去完全不同或者根本不可见,但是照相机的位置不会改变对象在全局空间中的位置。

    变换模型
    当您创建模型时,它们在场景中具有特定的位置。为了在场景中移动、旋转这些模型或者更改这些模型的大小而更改用来定义模型本身的顶点是不切实际的。相反,正如在二维对象中一样,您可以向模型应用变换。

    每个模型对象都有一个可用来对模型进行移动、重定向或调整大小的 Transform 属性。当您应用变换时,实际上是按照由变换功能指定的向量或值(以适用者为准)来偏移模型的所有点。换言之,您已经变换了在其中定义模型的坐标空间(“模型空间”),但是,您尚未更改在整个场景的坐标系(“全局空间”)中构成模型几何形状的值。

    平移变换
    三维变换继承自抽象基类 Transform3D;这些变换包括仿射变换类 TranslateTransform3D、ScaleTransform3D 和 RotateTransform3D。Windows Presentation Foundation (WPF) 三维系统还提供一个 MatrixTransform3D 类,使用该类,可以用更简明的矩阵操作来指定同样的变换。

    TranslateTransform3D 沿着您用 OffsetX、OffsetY 和 OffsetZ 属性指定的偏移向量所指示的方向来移动 Model3D 中的所有点。例如,假设立方体的一个顶点位于 (2,2,2),则偏移向量 (0,1.6,1) 会将该顶点 (2,2,2) 移到 (2,3.6,3)。该立方体的顶点在模型空间中仍位于 (2,2,2),但是现在,该模型空间与世界空间的关系已经发生改变,因此,模型空间中的 (2,2,2) 是世界空间中的 (2,3.6,3)。

    带有偏移的平移


    下面的代码示例演示如何应用平移。

    缩放变换
    ScaleTransform3D 沿着指定的缩放向量,相对于中心点来更改模型的比例。可以指定等比缩放,即在 X、Y 和 Z 轴上将模型缩放同样的值来按比例更改模型的大小。例如,将该变换的 ScaleX、ScaleY 和 ScaleZ 属性设置为 0.5 会将此模型二等分;将这些属性设置为 2 会将此模型在所有这三个轴上的值放大一倍。

    ScaleVector 示例


    通过指定非等比变换(X、Y 和 Z 值不相等的缩放变换),可以使模型在一个或两个维度上拉伸或收缩,而不会影响其他维度。例如,如果将 ScaleX、ScaleY 和 ScaleZ 分别设置为 1、2 和 1,则将导致变换模型的高度增加一倍,但是 X 和 Z 轴上的值仍保持不变。

    默认情况下,ScaleTransform3D 会导致顶点围绕原点 (0,0,0) 拉伸或收缩。但是,如果要变换的模型不是从原点绘制的,那么,在从原点缩放模型时,模型将不会“就地”缩放。相反,当模型的顶点与缩放向量相乘时,缩放操作对模型的平移和缩放都会产生影响。

    缩放中心示例

    若要“就地”缩放模型,请通过设置 ScaleTransform3D 的 CenterX、CenterY 和 CenterZ 属性来指定模型的中心。这可确保图形系统缩放模型空间,然后平移该空间,使其在指定的 Point3D 上居中。相反,如果模型是围绕原点生成的,而且您指定了一个不同的中心点,则将看到模型会背离原点平移。

    旋转变换
    可以通过几种不同的方法来旋转三维模型。典型的旋转变换指定一个轴以及围绕该轴的旋转角度。使用 RotateTransform3D 类,可以用 Rotation 属性来定义 Rotation3D。然后可以在 Rotation3D(在本例中为 AxisAngleRotation3D)上指定 Axis 和 Angle 属性来定义变换。下面的几个示例围绕 Y 轴将模型旋转 60 度。

    注意:Windows Presentation Foundation (WPF) 三维是一个右手系统,这意味着,如果旋转角度为正数,则将围绕该轴逆时针旋转。

    对于指定了轴和角度的旋转来说,如果没有为 RotateTransform3D 上的 CenterX、CenterY 和 CenterZ 属性指定值,则假设围绕原点旋转。与缩放时一样,一定要记住旋转时会变换模型的整个坐标空间。如果模型不是围绕原点创建的,或者它以前经过平移,则旋转可能会围绕原点“转动”,而不是就地旋转。

    在指定了新中心的情况下旋转


    若要“就地”旋转模型,请将模型的实际中心指定为旋转中心。由于几何形状通常是围绕原点建模的,因此,在依次执行下列操作时通常将获取一组预期的变换结果:调整模型大小(缩放该模型),然后设置模型方向(旋转该模型),最后将模型移到所需的位置(平移该模型)。

    旋转示例


    指定了轴和角度的旋转非常适于静态变换和某些动画。但是,请考虑围绕 X 轴将立方体模型旋转 60 度,然后围绕 Z 轴将其旋转 45 度。您可以将这种变换描述为两个离散的仿射变换,也可以将其描述为一个矩阵。但是,对于按照这种方式定义的旋转,可能很难平滑地进行动画处理。尽管按照这两种方法计算的模型起始位置和结束位置相同,但是,从计算的角度来看,该模型经过的中间位置是不确定的。四元数提供了一种用来在旋转的起始位置和结束位置之间计算内插值的替代方法。

    四元数表示三维空间中的一个轴以及围绕该轴的旋转。例如,四元数可以表示 (1,1,2) 轴以及 50 度的旋转角度。四元数在定义旋转方面的价值在于可以针对它们执行的两个运算:合成和内插。应用于一个几何形状的两个四元数的合成是指“围绕 axis2 将几何形状旋转 rotation2 度,然后围绕 axis1 将其旋转 rotation1 度”。通过使用合成运算,可以将应用于几何形状的两个旋转合成在一起,以便获得一个代表合成结果的四元数。由于四元数内插可以计算出从一个轴和方向到另一个轴和方向的平滑而又合理的路径,因此您可以从原始位置到合成的四元数之间进行内插,以便实现从一个位置到另一个位置的平滑过渡,从而可以对该变换进行动画处理。对于要进行动画处理的模型,可以通过针对 Rotation 属性使用 QuaternionRotation3D 来为旋转指定目标 Quaternion。

    使用变换集合
    在生成场景时,通常会向模型应用多个变换。向 Transform3DGroup 类的 Children 集合中添加变换,从而方便地将多个变换组合在一起以应用于场景中的各种模型。通常,可以很方便地在几个不同的组中重用某个变换,这与通过向每个实例应用一组不同的变换来重用模型大体相同。请注意,将变换添加到该集合中的顺序至关重要:集合中的变换是按照从第一个到最后一个的顺序应用的。

    对变换进行动画处理
    Windows Presentation Foundation (WPF) 三维实现与二维图形参与同一个计时和动画系统。换言之,若要对三维场景进行动画处理,需要对其模型的属性进行动画处理。可以直接对基元的属性进行动画处理,但是通常很容易对用来更改模型位置或外观的变换进行动画处理。由于可以向 Model3DGroup 对象及其各个模型应用变换,因此可以向 Model3DGroup 的子级应用一组动画,向一组对象应用另一组动画。

    若要对 Windows Presentation Foundation (WPF) 中的对象进行动画处理,可以创建时间线、定义动画(实际上是随着时间的推移而更改某个属性值)并指定要向其应用动画的属性。此属性必须是 FrameworkElement 的属性。由于三维场景中的所有对象都是 Viewport3D 的子级,因此要应用于场景的任何动画所面向的属性都是 Viewport3D 属性的属性。一定要仔细设计动画的属性路径,因为语法可能会极为冗长。

    假设您希望就地旋转某个对象,而且还希望应用摆动动作以公开要查看的对象的更多内容。您可以选择向模型应用 RotateTransform3D,并对模型从一个向量旋转到另一个向量时所围绕的轴进行动画处理。下面的代码示例演示如何将 Vector3DAnimation 应用于该变换的 Rotation3D 的 Axis 属性,并假设 RotateTransform3D 是应用于具有 TransformGroup 的模型的几个变换之一。

    使用类似的语法可以将其他变换属性作为目标来移动或缩放该对象。 例如,可以在进行缩放变换时将 Point3DAnimation 应用于 ScaleCenter 属性,以便使模型的形状平滑地扭曲。

    尽管上面的几个示例对 GeometryModel3D 的属性进行变换,但是还可以对场景中其他模型的属性进行变换。 例如,可以通过对应用于 Light 对象的平移进行动画处理来创建移动灯光和阴影效果,这些效果可以显著改变模型的外观。

    由于照相机也是模型,因此也可以对照相机的属性进行变换。 尽管您确实可以通过改变照相机的位置或平面距离来改变场景的外观(实际上是变换整个场景的投影),但应注意,对于观察者来说,以这种方法实现的许多效果不如将变换应用于场景中模型的地点或位置更有“视觉意义”。

    1. WPF的动画开发

    Windows Presentation Foundation (WPF) 提供了一组强大的图形和布局功能,通过应用这些功能,可以创建漂亮的用户界面和吸引人的文档。动画不仅可以使漂亮的用户界面更加引人注目,还可以使其更加便于使用。只需对背景色进行动画处理或应用动画 Transform,就可以创造出生动的屏幕过渡效果或提供有帮助的视觉提示。

    本概述介绍了 WPF 动画和计时系统,并使用演示图板重点讨论 WPF 对象的动画。

    本主题包括以下部分。

    动画简介
    动画是快速播放一系列图像(其中每个图像与下一个图像略微不同)给人造成的一种幻觉。大脑感觉这组图像是一个变化的场景。在电影中,摄像机每秒钟拍摄许多照片(帧),便可使人形成这种幻觉。用投影仪播放这些帧时,观众便可以看电影了。

    计算机上的动画与此类似。例如,使一个矩形逐渐从视野中消失的程序可能按以下方式工作。

    程序创建一个计时器。

    程序按照设置的时间间隔检查计时器以查看经历了多长时间。

    程序每次检查计时器时,它将根据经历的时间来计算矩形的当前不透明度值。

    然后程序用新值更新矩形并重画此矩形。

    在 WPF 出现之前,Microsoft Windows 开发人员必须创建和管理自己的计时系统或使用特殊的自定义库。WPF 包括一个通过托管代码和 可扩展应用程序标记语言 (XAML) 公开的高效计时系统,该系统紧密地集成到 WPF 框架中。通过使用 WPF 动画,可以轻松地对控件和其他图形对象进行动画处理。

    WPF 可以高效地处理管理计时系统和重绘屏幕的所有后台事务。它提供了计时类,使用这些类,可以重点关注要创造的效果,而非实现这些效果的方法。另外,WPF 通过公开动画基类(您的类可以继承自这些类)让您可以轻松创建自己的动画,这样便可以制作自定义动画。这些自定义的动画获得了标准动画类的许多性能优点。

    WPF 属性动画系统
    如果了解关于计时系统的一些重要概念,则在使用 WPF 时可能会更加轻松一些。最重要的是,在 WPF 中,通过对对象的个别属性应用动画,可以对对象进行动画处理。例如,若要使框架元素增大,请对其 Width 和 Height 属性进行动画处理。若要使对象逐渐从视野中消失,可以对其 Opacity 属性进行动画处理。

    要使属性具有动画功能,属性必须满足以下三个要求:

    它必须是依赖项属性。

    它必须属于继承自 DependencyObject 并实现 IAnimatable 接口的类。

    必须存在可用的兼容动画类型.

    WPF 包含许多具有动画属性的对象。诸如 Button、TabControl 和 Panel 控件以及 Shape 对象都继承自 DependencyObject。它们的大多数属性都是依赖项属性。

    您几乎可以在任何地方使用动画,包括在样式和控件模板中使用。动画未必可见;对于不属于用户界面的对象,只要它们满足本节中所述的条件,便可以对其进行动画处理。

    示例:使元素逐渐进入视野并从视野中逐渐消失
    此示例演示如何使用 WPF 动画对依赖项属性的值进行动画处理。本示例使用 DoubleAnimation(一种生成 Double 值的动画类型)对 Rectangle 的 Opacity 属性进行动画处理。因此,Rectangle 将逐渐进入视野并逐渐从视野中消失。

    示例的第一部分创建一个 Rectangle 元素,并将其显示在 Page 中。下面的步骤表明如何创建动画并将其应用于矩形的 Opacity 属性。

    第 1 部分:创建 DoubleAnimation
    使元素逐渐进入视野并逐渐从视野中消失的一种方法是对其 Opacity 属性进行动画处理。由于 Opacity 属性的类型是 Double,因此需要一个产生双精度值的动画。DoubleAnimation 就是这样的一个动画。DoubleAnimation 创建两个双精度值之间的过渡。若要指定其起始值,可设置其 From 属性。若要指定其终止值,可设置其 To 属性。

    不透明度值 1.0 使对象完全不透明,不透明度值 0.0 使对象完全不可见。若要使动画的不透明度值从 1.0 过渡为 0.0,可以将其 From 属性设置为 1.0,将其 To 属性设置为 0.0。

    [C#]

    然后,必须指定 Duration。动画的 Duration 指定了从其起始值过渡为目标值所需的时间。在下面的示例中,为动画指定的持续时间为五秒钟。

    上面的代码显示了不透明度值从 1.0 向 0.0 转换的动画,此转换使目标元素从完全不透明逐渐转变为完全不可见。若要使元素在消失后再逐渐回到视野中,请将动画的 AutoReverse 属性设置为 true。若要使动画无限期地重复,请将其 RepeatBehavior 属性设置为 Forever。

    第 2 部分:创建演示图板
    若要向对象应用动画,请创建 Storyboard 并使用 TargetName 和 TargetProperty 附加属性指定要进行动画处理的对象和属性。

    创建 Storyboard 并将动画添加为其子项。

    在代码中,将 Storyboard 声明为类成员。

    然后初始化 Storyboard 并将动画添加为其子项。

    必须知道要在哪里应用动画。使用 Storyboard..::.TargetName 附加属性指定要进行动画处理的对象。在下面的代码中,为 DoubleAnimation 指定了一个目标名称 MyRectangle,这是要进行动画处理的对象的名称。

      

    说明:
    在代码中创建演示图板时,必须执行两个附加步骤:创建名称范围以及注册要进行动画处理的对象的名称。本节开头的代码创建了页面和矩形,还声明了 NameScope 并注册了矩形的名称。如果还没有 NameScope,则可以使用 SetNameScope 方法创建一个。您可以使用 RegisterName 方法向为其创建了 NameScope 的元素注册目标对象的名称。否则,Storyboard 无法找到要进行动画处理的对象。

    使用 TargetProperty 附加属性指定要进行动画处理的属性。在下面的代码中,动画被配置为面向 Rectangle 的 Opacity 属性。

    第 3 部分 (XAML):将演示图板与触发器关联
    在 XAML 中应用和启动 Storyboard 的最简单的方法是使用事件触发器。

    创建一个 BeginStoryboard 对象并将演示图板与其关联。BeginStoryboard 是一种应用和启动 Storyboard 的 TriggerAction。

    创建 EventTrigger 并将 BeginStoryboard 添加至其 Actions 集合。将 EventTrigger 的 RoutedEvent 属性设置为启动 Storyboard 所需的路由事件。

    将 EventTrigger 添加至矩形的 Triggers 集合。

    第 3 部分(代码):将演示图板与事件处理程序关联
    在代码中应用和启动 Storyboard 的最简单的方法是使用事件处理程序。

    注册矩形的 Loaded 事件。

    [C#]

    声明事件处理程序。在事件处理程序中,使用 Begin 方法来应用演示图板。

    [C#]

    完整的示例

    下面的示例演示了一段完整代码,这段代码用于创建逐渐进入视野并从视野中逐渐消失的矩形。

    动画类型
    由于动画生成属性值,因此对于不同的属性类型,会有不同的动画类型。若要对采用 Double 的属性(例如元素的 Width 属性)进行动画处理,请使用生成 Double 值的动画。若要对采用 Point 的属性进行动画处理,请使用生成 Point 值的动画,依此类推。由于存在许多不同的属性类型,因此 System.Windows.Media.Animation 命名空间中存在一些动画类。幸好它们都遵循严格的命名约定,因此可以轻松地区分它们:

    <类型>Animation

    这些动画称为“From/To/By”或“基本”动画,它们在起始值和目标值之间进行动画处理,或者通过将偏移量值与其起始值相加来进行动画处理。

    若要指定起始值,请设置动画的“From”属性。

    若要指定终止值,请设置动画的“To”属性。

    若要指定偏移量值,请设置动画的“By”属性。

    此概述中的示例使用这些动画,因为这些动画使用起来最简单。From/To/By 动画概述中详细描述了“From/To/By”动画。

    <类型>AnimationUsingKeyFrames

    关键帧动画的功能比“From/To/By”动画的功能更强大,因为您可以指定任意多个目标值,甚至可以控制它们的插值方法。某些类型的内容只能用关键帧动画进行动画处理。关键帧动画概述中详细描述了关键帧动画。

    <类型>AnimationUsingPath

    路径动画使您能够使用几何路径来生成动画值。

    <类型>AnimationBase

    在实现时对 <类型> 值进行动画处理的抽象类。此类用作 <类型>Animation 和 <类型>AnimationUsingKeyFrames 类的基类。只有在想要创建自己的自定义动画时,才必须直接处理这些类。否则,请使用 <类型>Animation 或 KeyFrame<类型>Animation。

    在大多数情况下,您将需要使用 <类型>Animation 类,例如 DoubleAnimation 和 ColorAnimation。

    下表显示了一些常用动画类型以及与这些类型一起使用的一些属性。

    动画是时间线
    所有动画类型均继承自 Timeline 类,因此所有动画都是专用类型的时间线。Timeline 定义时间段。您可以指定时间线的以下计时行为:其 Duration 和重复次数,甚至可以为时间线指定时间走得多快。

    因为动画是 Timeline,所以它还表示一个时间段。在动画的指定时间段(即 Duration)内运行动画时,动画还会计算输出值。在运行或“播放”动画时,动画将更新与其关联的属性。

    Duration、AutoReverse 和 RepeatBehavior 是三个常用的计时属性。

    Duration 属性
    如前文所述,时间线代表一个时间段。该时间段的长度由时间线的 Duration(通常用 TimeSpan 值来指定)来决定。当时间线达到其持续时间的终点时,表示时间线完成了一次重复。

    动画使用其 Duration 属性来确定其当前值。如果没有为动画指定 Duration 值,它将使用默认值(1 秒)。

    下面的语法显示了 Duration 属性 (Property) 的 可扩展应用程序标记语言 (XAML) 属性 (Attribute) 语法的简化版本。

    下表显示了一些 Duration 设置及其结果值。

    在代码中指定 Duration 的一种方法是使用 FromSeconds 方法创建 TimeSpan,然后使用该 TimeSpan 声明新的 Duration 结构。

    AutoReverse
    AutoReverse 属性指定时间线在到达其 Duration 的终点后是否倒退。如果将此动画属性设置为 true,则动画在到达其 Duration 的终点后将倒退,即从其终止值向其起始值反向播放。默认情况下,该属性为 false。

    RepeatBehavior
    RepeatBehavior 属性指定时间线的播放次数。默认情况下,时间线的重复次数为 1.0,即播放一次时间线,根本不进行重复。

    对属性应用动画
    前面几节描述动画的不同类型及其计时属性。本节演示如何对要进行动画处理的属性应用动画。Storyboard 对象提供了对属性应用动画的一种方法。Storyboard 是一个为其所包含的动画提供目标信息的容器时间线。

    以对象和属性为目标
    Storyboard 类提供 TargetName 和 TargetProperty 附加属性。通过在动画上设置这些属性,您将告诉动画对哪些内容进行动画处理。但是,通常必须为对象提供一个名称,动画才能以该对象作为处理目标。

    为 FrameworkElement 分配名称不同于为 Freezable 对象分配名称。大多数控件和面板是框架元素;而大多数纯图形对象(如画笔、转换和几何图形)是可冻结的对象。

    若要使 FrameworkElement 成为动画目标,应通过设置其 Name 属性为其提供一个名称。在代码中,还必须使用 RegisterName 方法向元素名称所属的页面注册该元素名称。

    要使用 XAML 使 Freezable 对象成为目标,应使用 x:Name 属性为其分配一个名称。在代码中,只需使用 RegisterName 方法向对象所属的页面注册该对象。

    下面几节举例说明如何使用 XAML 和代码为元素命名。

    应用和启动演示图板
    若要使用 XAML 启动演示图板,应将其与 EventTrigger 关联。EventTrigger 是一个描述在发生指定事件时执行哪些操作的对象。这些操作中的一个操作可以是 BeginStoryboard 操作,您可以使用此操作启动演示图板。事件触发器与事件处理程序的概念类似,因为通过使用它们,您都可以指定应用程序如何响应特定事件。与事件处理程序不同的是,您完全可以使用 XAML 来描述事件触发器,而无需使用其他代码。

    若要使用代码启动 Storyboard,您可以使用 EventTrigger 或使用 Storyboard 类的 Begin 方法。

    以交互方式控制演示图板
    前面的示例演示如何在事件发生时启动 Storyboard。您也可以在 Storyboard 启动后以交互方式控制它:可以暂停、继续和停止它,并可以使其进入到其填充期,还可以查找和移除 Storyboard。

    动画结束后会发生什么情况?
    FillBehavior 属性指定时间线结束时的行为方式。默认情况下,时间线结束时将启动 Filling。处于 Filling 状态动画将保持其最终输出值。

    前面示例中的 DoubleAnimation 不会终止,因为它的 RepeatBehavior 属性设置为 Forever。下面的示例使用类似的动画对矩形进行动画处理。与前面的示例不同,此动画的 RepeatBehavior 和 AutoReverse 属性设为默认值。因此,动画运行五秒钟使其不透明度值从 1 转变成 0,然后停止。

    因为动画的 FillBehavior 未发生变化,仍然为其默认值 HoldEnd,所以当动画结束时动画将保持其最终值 0。因此,在动画结束后,矩形的 Opacity 仍然为 0。如果将矩形的 Opacity 设置为另一个值,则您的代码似乎无效,因为动画仍将影响 Opacity 属性。

    在代码中重新获得对动画属性的控制的一种方法是使用 BeginAnimation 方法,并将 AnimationTimeline 参数指定为 null。

    请注意,虽然设置一个具有 Active 或 Filling 动画的属性值好像不起作用,但属性值确实发生了变化。

    对动画进行数据绑定和动画处理
    您可以对大多数动画属性绑定数据或进行动画处理;例如,可以对 DoubleAnimation 的 Duration 属性进行动画处理。但是,由于计时系统工作方式的缘故,绑定有数据或进行过动画处理的动画的行为与其他绑定有数据或进行过动画处理的对象的行为不同。若要了解它们的行为,请了解对属性应用动画的意义,这将十分有用。

    加载前面示例中的矩形时,它的事件触发器将应用 Storyboard。计时系统会创建 Storyboard 及其动画的副本。系统将冻结这些副本(使它们成为只读副本),并且将根据它们来创建 Clock 对象。这些时钟将执行对目标属性进行动画处理的实际工作。

    计时系统为 DoubleAnimation 创建一个时钟,并将该时钟应用于 DoubleAnimation 的 TargetName 和 TargetProperty 所指定的对象和属性。在本例中,计时系统将时钟应用于名为“MyRectangle”的对象的 Opacity 属性。

    虽然也会为 Storyboard 创建一个时钟,但未对任何属性应用该时钟。该时钟的用途是控制其子时钟(为 DoubleAnimation 创建的时钟)。

    若要使动画反映数据绑定或动画更改,必须重新生成其时钟。系统不会为您自动生成时钟。若要使动画反映更改,请使用 BeginStoryboard 或 Begin 方法重新应用其演示图板。当使用其中的任何一种方法时,动画将重新启动。在代码中,可以使用 Seek 方法将演示图板移回到其从前的位置。

    其他动画处理方式
    此概述中的示例演示如何使用演示图板进行动画处理。如果使用代码,则可以采用一些其他方法进行动画处理。

    动画示例
    以下示例可以帮助您开始向应用程序添加动画。

    属性动画示例

    演示如何对框架元素和可冻结的对象应用动画。

    From/To/By 动画的目标值示例

    演示不同的 From/To/By 设置。

    动画计时行为的示例

    演示可控制动画计时行为的不同方法。此示例还演示如何绑定动画目标值数据。

    1. WPF的多媒体开发

    本主题将介绍 Windows Presentation Foundation (WPF) 的多媒体功能。使用多媒体功能可以将声音和视频集成到应用程序中,从而增强了用户体验。

    媒体 API
    MediaElement 和 MediaPlayer 用于播放音频、视频以及包含音频内容的视频。这两种类型都可以以交互方式或时钟驱动方式进行控制。这两种类型都至少依赖 Microsoft Windows Media Player 10 OCX 进行媒体播放。但这两种 API 的用法因具体情况而异。

    MediaElement 是一个 UIElement,它受 布局系统 支持并可用作许多控件的内容。它也可用在可扩展应用程序标记语言 (XAML) 以及代码中。另一方面,MediaPlayer 用于 Drawing 对象,因而缺少对布局的支持。只能使用 VideoDrawing 或通过直接与 DrawingContext 进行交互来呈现使用 MediaPlayer 加载的媒体。不能在 XAML 中使用 MediaPlayer。

    说明:
    如果将媒体与应用程序一起分发,则不能将媒体文件用作项目资源。在项目文件中,必须将媒体类型改设为 Content,并将 CopyToOutputDirectory 设置为 PreserveNewest 或 Always。

    媒体播放模式
    说明:
    MediaElement 和 MediaPlayer 具有类似的成员。本部分中的链接指的是 MediaElement 类成员。除非明确说明,否则链接到 MediaElement 类中的成员也可在 MediaPlayer 类中找到。

    若要了解 Windows Presentation Foundation (WPF) 中的媒体播放,需要先了解可播放媒体的不同模式。MediaElement 和 MediaPlayer 可以用于两种不同的媒体模式中:独立模式和时钟模式。媒体模式由 Clock 属性确定。如果 Clock 为 null,则媒体对象处于独立模式。如果 Clock 不为 null,则媒体对象处于时钟模式。默认情况下,媒体对象处于独立模式。

    独立模式
    在独立模式下,由媒体内容驱动媒体播放。独立模式实现了下列功能选项:

    可直接指定媒体的 Uri。

    可直接控制媒体播放。

    可修改媒体的 Position 和 SpeedRatio 属性。

    通过设置 MediaElement 对象的 Source 属性或者调用 MediaPlayer 对象的 Open 方法来加载媒体。

    若要在独立模式下控制媒体播放,可使用媒体对象的控制方法。提供了下列控制方法:Play、Pause、Close 和 Stop。对于 MediaElement,仅当将 LoadedBehavior 设置为 Manual 时,使用这些方法的交互式控件才可用。当媒体对象处于时钟模式时,这些方法将不可用。

    时钟模式
    在时钟模式下,由 MediaTimeline 驱动媒体播放。时钟模式具有下列特征:

    媒体的 Uri 是通过 MediaTimeline 间接设置的。

    可由时钟控制媒体播放。不能使用媒体对象的控制方法。

    可通过以下方法加载媒体:设置 MediaTimeline 对象的 Source 属性,从时间线创建时钟,并将时钟分配给媒体对象。当位于 Storyboard 中的 MediaTimeline 针对 MediaElement 时,也可用这种方法加载媒体。

    若要在时钟模式下控制媒体播放,必须使用 ClockController 控制方法。ClockController 是从 MediaClock 的 ClockController 属性获取的。如果尝试在时钟模式下使用 MediaElement 或 MediaPlayer 对象的控制方法,则会引发 InvalidOperationException。

    MediaElement
    向应用程序添加媒体的操作十分简单,只需向应用程序的用户界面 (UI) 添加 MediaElement 控件,并为要包含的媒体提供 Uri。Windows Presentation Foundation (WPF) 中支持 Microsoft Windows Media Player 10 所支持的所有媒体类型。下面的示例演示 MediaElement 在可扩展应用程序标记语言 (XAML) 中的简单用法。

    在此示例中,媒体在加载后即会自动播放。播放完后,就会关闭媒体,并且会释放所有媒体资源(包括视频内存)。此行为是 MediaElement 对象的默认行为,由 LoadedBehavior 和 UnloadedBehavior 属性控制。

    控制 MediaElement
    当 IsLoaded 为 true 或 false 时,可分别使用 LoadedBehavior 和 UnloadedBehavior 属性控制 MediaElement 的行为。设置 MediaState 属性的目的是影响媒体播放行为。例如,默认的 LoadedBehavior 为 Play,而默认的 UnloadedBehavior 为 Close。这意味着加载 MediaElement 并完成预播放后,即会开始播放媒体。播放完后,就会关闭媒体,并且会释放所有媒体资源。

    LoadedBehavior 和 UnloadedBehavior 属性不是控制媒体播放的唯一方法。在时钟模式下,时钟可以控制 MediaElement,并且这些交互式控制方法在 LoadedBehavior 为 Manual 时具有控制权。MediaElement 通过计算下列优先级来处理此控制权的竞争。

    UnloadedBehavior. 在卸载媒体时发生。这可确保默认情况下释放所有媒体资源,即使 MediaClock 与 MediaElement 关联也是如此。

    MediaClock. 在媒体具有 Clock 时发生。如果卸载媒体,则只要 UnloadedBehavior 为 Manual,MediaClock 就会生效。时钟模式始终重写 MediaElement 的加载行为。

    LoadedBehavior. 在加载媒体时发生。

    交互式控制方法。在 LoadedBehavior 为 Manual 时发生。提供了下列控制方法:Play、Pause、Close 和 Stop。

    显示 MediaElement
    若要显示 MediaElement,它必须具有要呈现的内容,并在加载内容之前将其 ActualWidth 和 ActualHeight 属性设置为零。对于仅包含音频的内容,这些属性将始终为零。对于视频内容,在 MediaOpened 事件引发 ActualWidth 和 ActualHeight 后,会报告已加载媒体的大小。这意味着在加载媒体之前,MediaElement 不会占用用户界面 (UI) 中的任何物理空间,除非设置了 Width 或 Height 属性。

    如果设置 Width 和 Height 属性,则会导致拉伸媒体来填充为 MediaElement 提供的区域。若要保持媒体的原始纵横比,应设置 Width 或 Height 属性,但不能同时设置这两者。如果同时设置 Width 和 Height 属性,则会使媒体以固定元素大小显示,可能无法达到预期效果。

    为避免元素大小固定,Windows Presentation Foundation (WPF) 可以预播放媒体。为此,需要将 LoadedBehavior 设置为 Play 或 Pause。在 Pause 状态下,媒体将预播放第一帧。在 Play 状态下,媒体将预播放,然后再开始播放。

     MediaPlayer
    当 MediaElement 类为框架元素时,MediaPlayer 类设计为在 Drawing 对象中使用。在可牺牲框架级功能来获得性能的提高或者需要 Freezable 功能时,可使用 Drawing 对象。通过 MediaPlayer,您可以在应用程序中提供媒体内容,同时使用这些功能。与 MediaElement 类似,MediaPlayer 可在独立模式或时钟模式下使用,但不具有 MediaElement 对象的卸载和加载状态。这会降低 MediaPlayer 的播放控制的复杂程度。

    控制 MediaPlayer
    由于 MediaPlayer 是无状态的,因此只能使用两种方法控制媒体播放。

    交互式控制方法。在处于独立模式(null Clock 属性)时采用。

    MediaClock. 在媒体具有 Clock 时采用。显示 MediaPlayer
    从技术角度来说,不能显示 MediaPlayer,因为它没有物理表示形式。但它可用于通过使用 VideoDrawing 类在 Drawing 中呈现媒体。下面的示例演示如何使用 VideoDrawing 显示媒体。

    展开全文
  • 天天敲代码会使人变聪明嚒?

    千次阅读 2019-01-31 11:43:28
    作者:一本正经哥来自:阿里巴巴中间件聪明绝顶出自《八仙全传》第五回,意为聪明有智慧,无人能超越。使用至今,有人用他来形容那些太聪明了以至于脱发的人。例如在程序员这个行当里...

    作者:一本正经哥

    来自:阿里巴巴中间件


    聪明绝顶出自《八仙全传》第五回,意为聪明有智慧,无人能超越。使用至今,有人用他来形容那些太聪明了以至于脱发的人。


    例如在程序员这个行当里,发量的多少常会被用来衡量程序员资深与否的重要标准之一。


    640?wx_fmt=png


    “代码敲的多,人就会变聪明。”


    这已成为众多资深程序员多年编程经验积累下来的武功秘籍,要想保持青春不老,就要多敲代码。


    所以大家都能看到,一般工作制度是6点下班,但程序员经常会加班至深夜,更有高阶程序员,半夜醒来敲代码,因为他们认为半夜醒来敲的代码才有灵魂。相比那些把 “一天不敲,心里发慌”挂在嘴边的入门级程序员相比,半夜敲代码才是进阶王道。

     

    常敲代码(编程)能使人聪明,并非空穴来风。


    640?wx_fmt=gif

     

    对于在亚特兰大担任全职投资银行分析师的亚当·维克斯曼而言,编程确实让他变得更加聪明,思考问题的逻辑性更强。


    2017年1月,亚当·维克斯曼将心中积累已久的想法付诸实践,开始通过网上教程自学C语言和JavaScript,那一年的夏天,他报名参加了一个编程训练营,并学会了Ruby on Rails。


    640?wx_fmt=png


    “学编程就像学外语,通过身临其境的学习,可以帮助我锻炼在创造力、批判性思维和逻辑思维方面的能力,我觉得自己现在的智商开始有点爆表!”

     

    亚当·维克斯曼并不是第一个通过学习编程来提高智商的人。


    640?wx_fmt=jpeg

     

    一位编程训练营的青年教师 Sherif Abushadi 说道:“每一年夏天,我们这里都会有来自各行各业的人来学习编程,有金融行业的大鳄,也有出处茅庐的嫩仔,虽然他们各自的背景有所不同,但都是为了学习编程而来,我能感受到他们的热情,他们会花一大半的时间反复制造并修复bug,直到他们都做对了。”

     

    通过编程能提高智商不仅有大量的实践案例,也有实验上的数据支撑。


    640?wx_fmt=jpeg


    一份来自麻省理工大学长达33页的《关于学习编程对认知能力的提升的研究》的报告中提到:研究者们统计了65项数据,并结合加权、对照等统计学方法,最终得出了学习编程的人比未学习编程的人的认知能力要高出16%的结论。


    很多医学工作者也参与到了这方面的研究中,他们认为大脑就像肌肉,需要反复的锻炼才能保持大脑的活力,通过锻炼可以保持神经元良好的润滑性和弹性,这也是医学领域的重要基础研究成果之一。


    640?wx_fmt=jpeg


    “我觉得通过编程的学习让自己变得更加聪明并不是一件特别神奇的事情,编程锻炼了我们的对复杂事情的分解能力,并通过一行行代码最终实现出一个功能,这是一个从抽象到具体,再从具体到抽象的过程”。医学工作者沙撸克 · 汗谈到。


    2014年,德国帕绍大学、美国卡内基·梅隆大学、乔治亚理工学院和莱布尼兹研究所联合进行了一项大脑测试试验。他们发现,编程有助于大脑中和语言、记忆、注意力以及逻辑相关的细胞的成长。


    640?wx_fmt=jpeg


    虽然大多数人认为编程只是数学和逻辑,但其实远不止于此。 编程涉及到注意力、语言、记忆、数学、逻辑和创造力等多方面的锻炼。长此以往的训练,让程序员群体具备了和常人不同的思维能力,非同一般。


    这份研究也指出,编程对程序员的思维能力产生了深远的影响,但也要注意期间的风险,先进的语言能给你带来很好的大脑训练,但落后的语言则会适得其反。


    学习编程不仅能使人更加聪明,还能防止大脑衰老。


    640?wx_fmt=png


    虽然有些人认为他们年龄太大了,已经错过了学习编程的最佳时机,但事实并非如此。


    伦敦的Head Squeeze团队的一项研究表明,人过了30,大脑就会开始萎缩,大脑物质不断的受到侵蚀,每年将失去大约0.5%的脑容量。 到90岁的时候,我们中的许多人已经失去了大约三分之一的大脑灰质。


    640?wx_fmt=jpeg


    “学习编程可以有效对抗阿尔茨海默氏症等机能衰退类的疾病,并防止神经衰弱。 大脑中的新生脑细胞的增长伴随着我们的一生,但增长速度因环境而异。想要保持大脑年轻,学习一门新技能是一个不错的办法,例如学习一种新的编程语,像Java、Python等,去挑战你从未接触过的技能,将会帮助我们的大脑形成新的神经连接,促进脑细胞的新城代谢。”年过40的 Janet Siegmund 医学博士抑制不住自己对编程的热情。

     

    近几年,编程的流行已经普及到了中小学生群体。

     

    新加坡的一家面向中小学生的在线编程类教育网站,推出了9-12岁和13-17岁两个适用于不同年龄阶段的编程课程,帮助学生们通过代码来实现自己的idea,并解决他们生活中发现的一些问题。


    640?wx_fmt=jpeg


    例如,这个学习小组发现新加坡的电力成本在上升于是他们构建了一个App,允许用户通过App来实时查看和控制自家电量的使用情况。同时还能设置一个电量最大使用值,一旦超出上限,就出现提示,另一个有趣的功能是,你还能看到各个设备使用电量的排行情况。


    那么,除了变得更聪明、更不易患老年痴呆症,程序员还有哪些优点?


    还有一点,丈母娘会因为自己的女婿拥有一手修电脑的好活而在邻里之间沾沾自喜。

    架构师小秘圈

    聚集20万架构师的小圈子

    640?wx_fmt=jpeg

    长按二维码 ▲

    关注「架构师小秘圈」公众号

    展开全文
  • 卷积神经网络入门详解

    万次阅读 多人点赞 2017-11-08 15:52:14
    在这里我主要记录下自己觉得重要的内容以及一些相关的想法,希望能与大家多多交流~ 0. 回顾之前所写过的博客   在之前的博客《十四、卷积神经网络(1):介绍卷积神经网络》《十五、卷积神经网络(2):卷积...

      本文主要内容为 CS231n 课程的学习笔记,主要参考 学习视频 和对应的 课程笔记翻译 ,感谢各位前辈对于深度学习的辛苦付出。在这里我主要记录下自己觉得重要的内容以及一些相关的想法,希望能与大家多多交流~

    0. 回顾之前所写过的博客

      在之前的博客《十四、卷积神经网络(1):介绍卷积神经网络》《十五、卷积神经网络(2):卷积神经网络的结构》中只是介绍性的给出了一些关于卷积神经网络的知识。这主要因为《Neural Networks and Deep Learning》这本书中的大部分章节在介绍神经网络,而仅在最后一个部分介绍了卷积神经网络,只介绍了卷积神经网络中的相关概念,如“感受野”、“卷积层”、“池化层”、“权值共享”等等,而且也只介绍了一种较为简单的卷积神经网络结构。而且在这上述两节内容中的图像也是单通道的图像,并没有考虑多通道图像。

      作为补充和完善,在本文中将对卷积神经网络中的多个问题具体展开讲解。

    1. 卷积神经网络结构概述

      如果用全连接神经网络处理大尺寸图像具有三个明显的缺点:

      (1)将图像展开为向量会丢失空间信息;

      (2)参数过多效率低下,训练困难;

      (3)大量的参数也很快会导致网络过拟合。

    而使用卷积神经网络可以很好地解决上面的三个问题。

      与全连接神经网络不同,卷积神经网络的各层中的神经元是3维排列的:宽度、高度和深度。其中的宽度和高度是很好理解的,因为本身卷积就是一个二维模板,但是在卷积神经网络中的深度指的是激活数据体的第三个维度,而不是整个网络的深度,整个网络的深度指的是网络的层数。

      举个例子来理解什么是宽度,高度和深度,假如使用 CIFAR-10 中的图像是作为卷积神经网络的输入,该输入数据体的维度是32x32x3(宽度,高度和深度)。**卷积神经网络中,层中的神经元将只与前一层中的一小块区域连接,而不是采取全连接方式。**对于用来分类CIFAR-10中的图像的卷积网络,其最后的输出层的维度是1x1x10,因为在卷积神经网络结构的最后部分将会把全尺寸的图像压缩为包含分类评分的一个向量,向量是在深度方向排列的。下面是例子:

    图 1. 全连接神经网络与卷积神经网络的对比

    图1中左侧是一个3层的神经网络;右侧是一个卷积神经网络,将它的神经元在成3个维度(宽、高和深度)进行排列。卷积神经网络的每一层都将3D的输入数据变化为神经元3D的激活数据并输出。在图1的右侧,红色的输入层代表输入图像,所以它的宽度和高度就是图像的宽度和高度,它的深度是3(代表了红、绿、蓝3种颜色通道),与红色相邻的蓝色部分是经过卷积和池化之后的激活值(也可以看做是神经元) ,后面是接着的卷积池化层。

    2. 构建卷积神经网络的各种层

      卷积神经网络主要由这几类层构成:输入层、卷积层,ReLU层、池化(Pooling)层和全连接层(全连接层和常规神经网络中的一样)。通过将这些层叠加起来,就可以构建一个完整的卷积神经网络。在实际应用中往往将卷积层与ReLU层共同称之为卷积层,所以卷积层经过卷积操作也是要经过激活函数的。具体说来,卷积层和全连接层(CONV/FC)对输入执行变换操作的时候,不仅会用到激活函数,还会用到很多参数,即神经元的权值w和偏差b;而ReLU层和池化层则是进行一个固定不变的函数操作。卷积层和全连接层中的参数会随着梯度下降被训练,这样卷积神经网络计算出的分类评分就能和训练集中的每个图像的标签吻合了。

    2.1 卷积层

      卷积层是构建卷积神经网络的核心层,它产生了网络中大部分的计算量。注意是计算量而不是参数量

    2.1.1 卷积层作用

      1. 滤波器的作用或者说是卷积的作用。卷积层的参数是有一些可学习的滤波器集合构成的。每个滤波器在空间上(宽度和高度)都比较小,但是深度和输入数据一致(这一点很重要,后面会具体介绍)。直观地来说,网络会让滤波器学习到当它看到某些类型的视觉特征时就激活,具体的视觉特征可能是某些方位上的边界,或者在第一层上某些颜色的斑点,甚至可以是网络更高层上的蜂巢状或者车轮状图案。

      2. 可以被看做是神经元的一个输出。神经元只观察输入数据中的一小部分,并且和空间上左右两边的所有神经元共享参数(因为这些数字都是使用同一个滤波器得到的结果)。

      3. 降低参数的数量。这个由于卷积具有“权值共享”这样的特性,可以降低参数数量,达到降低计算开销,防止由于参数过多而造成过拟合。

    2.1.2 感受野(重点理解)

      在处理图像这样的高维度输入时,让每个神经元都与前一层中的所有神经元进行全连接是不现实的。相反,我们让每个神经元只与输入数据的一个局部区域连接。该连接的空间大小叫做神经元的感受野(receptive field),它的尺寸是一个超参数(其实就是滤波器的空间尺寸)。在深度方向上,这个连接的大小总是和输入量的深度相等。需要再次强调的是,我们对待空间维度(宽和高)与深度维度是不同的:连接在空间(宽高)上是局部的,但是在深度上总是和输入数据的深度一致,这一点会在下面举例具体说明。

    图 2. 举例说明感受野的连接及尺寸说明

      在图 2 中展现的卷积神经网络的一部分,其中的红色为输入数据,假设输入数据体尺寸为[32x32x3](比如CIFAR-10的RGB图像),如果感受野(或滤波器尺寸)是5x5,那么卷积层中的每个神经元会有输入数据体中[5x5x3]区域的权重,共5x5x3=75个权重(还要加一个偏差参数)。注意这个连接在深度维度上的大小必须为3,和输入数据体的深度一致。其中还有一点需要注意,对应一个感受野有75个权重,这75个权重是通过学习进行更新的,所以很大程度上这些权值之间是不相等(也就是对于某一个特定的卷积核,在它输入数据体的每一个深度切片上的权重都是独特的,不是将 N × N × 1 N \times N \times 1 N×N×1 重复输入数据题的深度那么多次就可以的)。在卷积神经网络中,某一个特定的卷积核,可以根据输入数据题体的维度,拆解为一个一个传统意义上的 N × N × 1 N \times N \times 1 N×N×1 的卷积模板,输入数据题的深度切片与和自己对应的卷积模板做完卷积之后,再将各个深度的结果加起来,再加上偏置(注意是一个偏置,无论输入输入数据是多少层,一个卷积核就对应一个偏置)就获得了这个卷基层卷积之后的结果。

    2.1.3 神经元的空间排列

      感受野讲解了卷积层中每个神经元与输入数据体之间的连接方式,但是尚未讨论输出数据体中神经元的数量,以及它们的排列方式。3个超参数控制着输出数据体的尺寸:深度(depth),步长(stride)和零填充(zero-padding)。

      (1) 输出数据体的深度:它是一个超参数,和使用的滤波器的数量一致,而每个滤波器在输入数据中寻找一些不同的东西,即图像的某些特征。如图2 所示,将沿着深度方向排列、感受野相同的神经元集合称为深度列(depth column),也有人使用纤维(fibre)来称呼它们。

      (2) 在滑动滤波器的时候,必须指定步长。当步长为1,滤波器每次移动1个像素;当步长为2,滤波器滑动时每次移动2个像素,当然步长也可以是不常用的3,或者更大的数字,但这些在实际中很少使用 ,这个操作会让输出数据体在空间上变小。

      (3) 有时候将输入数据体用 0 在边缘处进行填充是很方便的。这个零填充(zero-padding)的尺寸是一个超参数。零填充有一个良好性质,即可以控制输出数据体的空间尺寸与输入数据体的空间尺寸相同,使得输入和输出的宽高都相等。但是要注意,只能保证宽和高相等,并不能保证深度,刚刚讲过,深度与滤波器(或者称卷积核)的数量有关。

      输出数据体在空间上的尺寸 W 2 × H 2 × D 2 W_2 \times H_2 \times D_2 W2×H2×D2 可以通过输入数据体尺寸 W 1 × H 1 × D 1 W_1 \times H_1 \times D_1 W1×H1×D1,卷积层中神经元的感受野尺寸(F),步长(S),滤波器数量(K)和零填充的数量(P)计算输出出来。

    W 2 = ( W 1 − F + 2 P ) / S + 1 H 2 = ( H 1 − F + 2 P ) / S + 1 D 2 = K \begin{matrix} W_2=(W_1-F+2P)/S+1 \\ H_2=(H_1-F+2P)/S+1 \\ D_2=K \\ \end{matrix} W2=(W1F+2P)/S+1H2=(H1F+2P)/S+1D2=K

      一般说来,当步长S=1时,零填充的值是P=(F-1)/2,这样就能保证输入和输出数据体有相同的空间尺寸。

      步长的限制:注意这些空间排列的超参数之间是相互限制的。举例说来,当输入尺寸W=10,零填充 P=0,滤波器尺寸 F=3,此时步长 S=2 是行不通,因为 (W-F+2P)/S+1=(10-3+0)/2+1=4.5,结果不是整数,也就是说神经元不能整齐对称地滑过输入数据体。因此,这些超参数的设定就被认为是无效的,一个卷积神经网络库可能会报出一个错误,通过修改零填充值、修改输入数据体尺寸,或者其他什么措施来让设置合理。在后面的卷积神经网络结构小节中,读者可以看到合理地设置网络的尺寸让所有的维度都能正常工作,是相当让人头痛的事;而使用零填充和遵守其他一些设计策略将会有效解决这个问题。

    2.1.4 权值共享

      在卷积层中权值共享被用来控制参数的数量。假如在一个卷积核中,每一个感受野采用的都是不同的权重值(卷积核的值不同),那么这样的网络中参数数量将是十分巨大的。

      权值共享是基于这样的一个合理的假设:如果一个特征在计算某个空间位置 ( x 1 , y 1 ) (x_1,y_1) (x1,y1) 的时候有用,那么它在计算另一个不同位置 ( x 2 , y 2 ) (x_2,y_2) (x2,y2) 的时候也有用。基于这个假设,可以显著地减少参数数量。举个例子,我们将深度维度上一个单独的2维切片看做深度切片(depth slice),对于一个经过卷积后尺寸为 [55x55x96] 的数据体就有96个深度切片,每个尺寸为[55x55],其中在每个深度切片上的结果都使用同样的权重和偏差获得的。对于 [55x55x96] 的这个数据体,我们可以知道他经过的卷基层有96个卷积核,也就是对应 96 个不同的权重集了,一个权重集对应一个深度切片,如果卷积核的大小是 11x11的,图像是RGB 3 通道的,那么就共有96x11x11x3=34,848个不同的权重,总共有34,944个参数(因为要+96个偏差),并且在每个深度切片中的55x55 的结果使用的都是同样的参数(即权值共享)。

      在反向传播的时候,都要计算每个神经元对它的权重的梯度,但是需要把同一个深度切片上的所有神经元对权重的梯度累加,这样就得到了对共享权重的梯度。这样,每个切片只更新一个权重集。这样做的原因可以通过下面这张图进行解释

    图 3. 将卷积层用全连接层的形式表示

    如上图所示,左侧的神经元是将每一个感受野展开为一列之后串联起来(就是展开排成一列,同一层神经元之间不连接)。右侧的 Deep1i 是深度为1的神经元的第 i 个, Deep2i 是深度为2的神经元的第 i 个,同一个深度的神经元的权值都是相同的,黄色的都是相同的(上面4个与下面4个的参数相同),蓝色都是相同的。所以现在回过头来看上面说的卷积神经网络的反向传播公式对梯度进行累加求和也是基于这点考虑(同一深度的不同神经元共用一组参数,所以累加);而每个切片只更新一个权重集的原因也是这样的,因为从图3 中可以看到,不同深度的神经元不会公用相同的权重,所以只能更新一个权重集。

      注意,如果在一个深度切片中的所有权重都使用同一个权重向量,那么卷积层的前向传播在每个深度切片中可以看做是在计算神经元权重和输入数据体的卷积(这就是“卷积层”名字由来)。这也是为什么总是将这些权重集合称为滤波器(filter)(或卷积核(kernel)),因为它们和输入进行了卷积。

      注意,有时候参数共享假设可能没有意义,特别是当卷积神经网络的输入图像是一些明确的中心结构时候。这时候我们就应该期望在图片的不同位置学习到完全不同的特征(而一个卷积核滑动地与图像做卷积都是在学习相同的特征)。一个具体的例子就是输入图像是人脸,人脸一般都处于图片中心,而我们期望在不同的位置学习到不同的特征,比如眼睛特征或者头发特征可能(也应该)会在图片的不同位置被学习。在这个例子中,通常就放松参数共享的限制,将层称为局部连接层(Locally-Connected Layer)。

    2.1.5 卷积层的超参数及选择

      由于参数共享,每个滤波器包含 F ⋅ F ⋅ D 1 F \cdot F\cdot D_1 FFD1 个权重(字符的具体含义在2.1.3中有介绍),卷积层一共有$ F\cdot F\cdot D_1\cdot K$个权重和 K K K 个偏置。在输出数据体中,第d个深度切片(空间尺寸是 W 2 × H 2 W_2\times H_2 W2×H2),用第d个滤波器和输入数据进行有效卷积运算的结果(使用步长S),最后在加上第d个偏差。

      对这些超参数,常见的设置是 F = 3 , S = 1 , P = 1 F=3,S=1,P=1 F=3S=1P=1。同时设置这些超参数也有一些约定俗成的惯例和经验,可以在下面的“卷积神经网络结构”中查看。

    2.1.6 卷积层演示

      因为3D数据难以可视化,所以所有的数据(输入数据体是蓝色,权重数据体是红色,输出数据体是绿色)都采取将深度切片按照列的方式排列展现。输入数据体的尺寸是 W 1 = 5 , H 1 = 5 , D 1 = 3 W_1=5,H_1=5,D_1=3 W1=5,H1=5,D1=3,卷积层参数 K = 2 , F = 3 , S = 2 , P = 1 K=2,F=3,S=2,P=1 K=2,F=3,S=2,P=1。就是说,有2个滤波器,滤波器的尺寸是 3 ⋅ 3 3\cdot 3 33,它们的步长是2。因此,输出数据体的空间尺寸是 ( 5 − 3 + 2 ) / 2 + 1 = 3 (5-3+2)/2+1=3 (53+2)/2+1=3。注意输入数据体使用了零填充 P = 1 P=1 P=1,所以输入数据体外边缘一圈都是0。下面的例子在绿色的输出激活数据上循环演示,展示了其中每个元素都是先通过蓝色的输入数据和红色的滤波器逐元素相乘,然后求其总和,最后加上偏差得来。

    无法正常显示,请参考http://cs231n.github.io/convolutional-networks/ 的动图
    图 4. 卷积层演示过程

    2.1.7 用矩阵乘法实现卷积

      卷积运算本质上就是在滤波器和输入数据的局部区域间做点积。卷积层的常用实现方式就是利用这一点,将卷积层的前向传播变成一个巨大的矩阵乘法。

      (1) 输入图像的局部区域被 i m 2 c o im2co im2co l操作拉伸为列。比如输入是[227x227x3],要与尺寸为11x11x3的滤波器以步长为4进行卷积,就依次取输入中的[11x11x3]数据块,然后将其拉伸为长度为11x11x3=363的列向量。重复进行这一过程,因为步长为4,所以经过卷积后的宽和高均为(227-11)/4+1=55,共有55x55=3,025个个神经元。因为每一个神经元实际上都是对应有 363 的列向量构成的感受野,即一共要从输入上取出 3025 个 363 维的列向量。所以经过im2col操作得到的输出矩阵 X c o l X_col Xcol 的尺寸是[363x3025],其中每列是拉伸的感受野。注意因为感受野之间有重叠,所以输入数据体中的数字在不同的列中可能有重复。

      (2) 卷积层的权重也同样被拉伸成行。举例,如果有96个尺寸为[11x11x3]的滤波器,就生成一个矩阵 W r o w W_row Wrow,尺寸为[96x363]。

      (3) 现在卷积的结果和进行一个大矩阵乘法 n p . d o t ( W r o w , X c o l ) np.dot(W_row, X_col) np.dot(Wrow,Xcol) 是等价的了,能得到每个滤波器和每个感受野间的点积。在我们的例子中,这个操作的输出是[96x3025],给出了每个滤波器在每个位置的点积输出。注意其中的 n p . d o t np.dot np.dot 计算的是矩阵乘法而不是点积。

      (4) 结果最后必须被重新变为合理的输出尺寸[55x55x96]。

      这个方法的缺点就是占用内存太多,因为在输入数据体中的某些值在 X c o l X_col Xcol中被复制了多次;优点在于矩阵乘法有非常多的高效底层实现方式(比如常用的BLAS API)。还有,同样的im2col思路可以用在池化操作中。反向传播:卷积操作的反向传播(同时对于数据和权重)还是一个卷积(但是和空间上翻转的滤波器),所以依然可以使用矩阵相乘来计算卷积的反向传播。

      为什么卷积的反向传播还是卷积,这个可以参考另一篇文章 《CNN中卷积层与转置卷积层的关系(转置卷积又称反卷积、分数步长卷积)》 2.2 卷积的前向传播和反向传播 中的内容。

    2.1.8 其他形式的卷积操作

      1x1卷积:一些论文中使用了1x1的卷积,这个方法最早是在论文Network in Network中出现。人们刚开始看见这个1x1卷积的时候比较困惑,尤其是那些具有信号处理专业背景的人。因为信号是2维的,所以1x1卷积就没有意义。但是,在卷积神经网络中不是这样,因为这里是对3个维度进行操作,滤波器和输入数据体的深度是一样的。比如,如果输入是[32x32x3],那么1x1卷积就是在高效地进行3维点积(因为输入深度是3个通道);另外的一种想法是将这种卷积的结果看作是全连接层的一种实现方式,详见本文2.4.2 部分。

      扩张卷积:最近一个研究(Fisher Yu和Vladlen Koltun的论文)给卷积层引入了一个新的叫扩张(dilation)的超参数。到目前为止,我们只讨论了卷积层滤波器是连续的情况。但是,让滤波器中元素之间有间隙也是可以的,这就叫做扩张。如图5 为进行1扩张。

    图 4. 扩张卷积的例子及扩张前后的叠加效果

    在某些设置中,扩张卷积与正常卷积结合起来非常有用,因为在很少的层数内更快地汇集输入图片的大尺度特征。比如,如果上下重叠2个3x3的卷积层,那么第二个卷积层的神经元的感受野是输入数据体中5x5的区域(可以成这些神经元的有效感受野是5x5,如图5 所示)。如果我们对卷积进行扩张,那么这个有效感受野就会迅速增长。

    2.2 池化层

      通常在连续的卷积层之间会周期性地插入一个池化层。它的作用是逐渐降低数据体的空间尺寸,这样的话就能减少网络中参数的数量,使得计算资源耗费变少,也能有效控制过拟合。汇聚层使用 MAX 操作,对输入数据体的每一个深度切片独立进行操作,改变它的空间尺寸。最常见的形式是汇聚层使用尺寸2x2的滤波器,以步长为2来对每个深度切片进行降采样,将其中75%的激活信息都丢掉。每个MAX操作是从4个数字中取最大值(也就是在深度切片中某个2x2的区域),深度保持不变。

      汇聚层的一些公式:输入数据体尺寸 W 1 ⋅ H 1 ⋅ D 1 W_1\cdot H_1\cdot D_1 W1H1D1,有两个超参数:空间大小 F F F和步长 S S S;输出数据体的尺寸 W 2 ⋅ H 2 ⋅ D 2 W_2\cdot H_2\cdot D_2 W2H2D2,其中
    W 2 = ( W 1 − F ) / S + 1 H 2 = ( H 1 − F ) / S + 1 D 2 = D 1 \begin{matrix} W_2=(W_1-F)/S+1 \\ H_2=(H_1-F)/S+1 \\ D_2=D1 \\ \end{matrix} W2=(W1F)/S+1H2=(H1F)/S+1D2=D1
    这里面与之前的卷积的尺寸计算的区别主要在于两点,首先在池化的过程中基本不会进行另补充;其次池化前后深度不变。

      在实践中,最大池化层通常只有两种形式:一种是 F = 3 , S = 2 F=3,S=2 F=3,S=2,也叫重叠汇聚(overlapping pooling),另一个更常用的是 F = 2 , S = 2 F=2,S=2 F=2,S=2。对更大感受野进行池化需要的池化尺寸也更大,而且往往对网络有破坏性。
      普通池化(General Pooling):除了最大池化,池化单元还可以使用其他的函数,比如平均池化(average pooling)或L-2范式池化(L2-norm pooling)。平均池化历史上比较常用,但是现在已经很少使用了。因为实践证明,最大池化的效果比平均池化要好。

      反向传播:回顾一下反向传播的内容,其中max(x,y)函数的反向传播可以简单理解为将梯度只沿最大的数回传。因此,在向前传播经过汇聚层的时候,通常会把池中最大元素的索引记录下来(有时这个也叫作道岔(switches)),这样在反向传播的时候梯度的时候就很高效。

      下面将简单介绍平均池化和最大池化反向传播的计算过程。首先是平均池化,在前向传播的过程中,是计算窗口中的均值,很容易理解;在反向传播的过程中,是将要反向传播的梯度组分在平均分在四个部分传回去,具体如下所示

    forward: [1 3; 2 2] -> [2]
    backward: [2] -> [0.5 0.5; 0.5 0.5]
    

    对于最大池化,就如上面所说的,会把池中最大元素的索引记录下来,在反向传播的时候只对这样的位置进行反向传播,忽略其他的位置

    forward: [1 3; 2 2] -> 3
    backward: [3] -> [0 3; 0 0]
    

      不使用汇聚层:很多人不喜欢汇聚操作,认为可以不使用它。比如在Striving for Simplicity: The All Convolutional Net一文中,提出使用一种只有重复的卷积层组成的结构,抛弃汇聚层。通过在卷积层中使用更大的步长来降低数据体的尺寸。有发现认为,在训练一个良好的生成模型时,弃用汇聚层也是很重要的。比如变化自编码器(VAEs:variational autoencoders)和生成性对抗网络(GANs:generative adversarial networks)。现在看起来,未来的卷积网络结构中,可能会很少使用甚至不使用汇聚层

    2.3 归一化层

      在卷积神经网络的结构中,提出了很多不同类型的归一化层,有时候是为了实现在生物大脑中观测到的抑制机制。但是这些层渐渐都不再流行,因为实践证明它们的效果即使存在,也是极其有限的

    2.4 全连接层

      这个和常规神经网络中的一样,它们的激活可以先用矩阵乘法,再加上偏差。

    2.4.1 将卷积层转化成全连接层

      对于任意一个卷积层,都存在一个能实现和它一样的前向传播函数的全连接层。该全连接层的权重是一个巨大的矩阵,除了某些特定块(感受野),其余部分都是零;而在非 0 部分中,大部分元素都是相等的(权值共享),具体可以参考图3。如果把全连接层转化成卷积层,以输出层的 Deep11 为例,与它有关的输入神经元只有上面四个,所以在权重矩阵中与它相乘的元素,除了它所对应的4个,剩下的均为0,这也就解释了为什么权重矩阵中有为零的部分;另外要把“将全连接层转化成卷积层”和“用矩阵乘法实现卷积”区别开,这两者是不同的,后者本身还是在计算卷积,只不过将其展开为矩阵相乘的形式,并不是"将全连接层转化成卷积层",所以除非权重中本身有零,否则用矩阵乘法实现卷积的过程中不会出现值为0的权重。

    2.4.2 将全连接层转化成卷积层

      任何全连接层都可以被转化为卷积层。比如,一个K=4096的全连接层,输入数据体的尺寸是 7 × 7 × 512 7\times 7\times 512 7×7×512,这个全连接层可以被等效地看做一个 F = 7 , P = 0 , S = 1 , K = 4096 F=7,P=0,S=1,K=4096 F=7,P=0,S=1,K=4096的卷积层。换句话说,就是将滤波器的尺寸设置为和输入数据体的尺寸设为一致的。因为只有一个单独的深度列覆盖并滑过输入数据体,所以输出将变成 1 × 1 × 4096 1\times 1\times 4096 1×1×4096,这个结果就和使用初始的那个全连接层一样了。这个实际上也很好理解,因为,对于其中的一个卷积滤波器,这个滤波器的的深度为512,也就是说,虽然这个卷积滤波器的输出只有1个,但是它的权重有 7 × 7 × 512 7\times 7\times 512 7×7×512,相当于卷积滤波器的输出为一个神经元,这个神经元与上一层的所有神经元相连接,而这样与前一层所有神经元相连接的神经元一共有4096个,这不就是一个全连接网络嘛~

      在上述的两种变换中,将全连接层转化为卷积层在实际运用中更加有用。假设一个卷积神经网络的输入是224x224x3的图像,一系列的卷积层和汇聚层将图像数据变为尺寸为7x7x512的激活数据体(在AlexNet中就是这样,通过使用5个汇聚层来对输入数据进行空间上的降采样,每次尺寸下降一半,所以最终空间尺寸为224/2/2/2/2/2=7)。从这里可以看到,AlexNet使用了两个尺寸为4096的全连接层,最后一个有1000个神经元的全连接层用于计算分类评分。我们可以将这3个全连接转化为3个卷积层:

      (1) 针对第一个连接区域是[7x7x512]的全连接层,令其滤波器尺寸为F=7,这样输出数据体就为[1x1x4096]了。

      (2) 针对第二个全连接层,令其滤波器尺寸为F=1,这样输出数据体为[1x1x4096]。

      (3) 对最后一个全连接层也做类似的,令其F=1,最终输出为[1x1x1000]。

      这样做的目的是让卷积网络在一张更大的输入图片上滑动,得到多个输出,这样的转化可以让我们在单个向前传播的过程中完成上述的操作。

      举个例子,如果我们想让224x224尺寸的浮窗,以步长为32在384x384的图片上滑动,把每个经停的位置都带入卷积网络,最后得到6x6个位置的类别得分。上述的把全连接层转换成卷积层的做法会更简便。如果224x224的输入图片经过卷积层和汇聚层之后得到了[7x7x512]的数组,那么,384x384的大图片直接经过同样的卷积层和汇聚层之后会得到[12x12x512]的数组(因为途径5个汇聚层,尺寸变为384/2/2/2/2/2 = 12)。然后再经过上面由3个全连接层转化得到的3个卷积层,最终得到[6x6x1000]的输出(因为(12 - 7)/1 + 1 = 6)。这个结果正是浮窗在原图经停的6x6个位置的得分!

      面对384x384的图像,让(含全连接层)的初始卷积神经网络以32像素的步长独立对图像中的224x224块进行多次评价,其效果和使用把全连接层变换为卷积层后的卷积神经网络进行一次前向传播是一样的。相较于使用被转化前的原始卷积神经网络对所有36个位置进行迭代计算,使用转化后的卷积神经网络进行一次前向传播计算要高效得多,因为36次计算都在共享计算资源。

      这里有几个问题,首先为什么是以32为步长,如果我以64为步长呢?再或者如果我们想用步长小于32(如16)的浮窗怎么办

      首先回答其中的第一个问题。这个是因为其中一个有五个汇聚层,因为 2 5 = 32 2^5=32 25=32,也就是在原始图像上的宽或者高增加 32 32 32 个像素,经过这些卷积和汇聚后,将变为一个像素。现在进行举例说明,虽然例子并没有32那么大的尺寸,但是意义都是一样的。假设较小的输入图像尺寸为 4×4,进行一次卷积核为 F = 3 , S = 1 , P = 1 F=3, S=1, P=1 F=3,S=1,P=1的卷积后,再进行一次 2 × 2 2\times 2 2×2 的 maxpooling;然后在进行一次这样的卷积,再进行一次池化。假设较大的原始图像的尺寸为 8×8,以步长为4进行滑窗操作,对于每个窗内进行同样的卷积池化操作,如图5所示

    图 5. 以步长为4在原始图像上滑动取出4×4窗口再计算卷积的结果

    对较小的原始图像(图5左图红框)进行卷积得到的结果是图5右图红色框内的结果,通过划窗取样再进行卷积运算获得的结果为图5中右侧四种颜色加在一起的样子。所以以步长为4在8x8的图片上滑动,把每个经停的位置都带入卷积网络,最后得到2x2个位置的卷积结果,但是如果直接使用卷积核 F = 3 , S = 1 , P = 1 F=3, S=1, P=1 F=3,S=1,P=1进行两次卷积池化的话,得到的结果的大小显然也是2×2的。

      所以从获得结果来看,这两者是相同的,但是不同点在哪呢?如图6所示,是在整个图像上进行卷积运算和以步长为4在8x8的图片上滑动所经停的第一个位置,这两种方法使用相同的卷积核进行计算的对比图。

    这里写图片描述
    图6. 使用整张图像和一部分图像计算某一点处的卷积

    如图6所示,左图代表使用整张图像时计算a点处的卷积,右图代表使用滑动的方法第一次经停图像上a点的卷积,两张图中的a点是同一个a点。虽然同一个卷积模板进行计算,但是在计算卷积的过程是不同的!因为在右图中a的右侧及右下侧是0,而在左图中是原始的像素值,所以计算的卷积一定是不同的。但是要怎么理解这样的差别呢?这要从补零的意义讲起,补零是因为如果不补零的话,图像经过卷积之后的尺寸会小于原始的尺寸,补零可以保证图像的尺寸不变,所以归根结底补零实际上是一种图像填充的方法。左图中a的右边及右下角的像素是原始图像的像素,相当于在计算a点的时候,不是用0进行的补充,而是原始像素值进行补充,这样的不仅可以保持卷积前后图像的大小不变,而且可以这种图像填充方法得到的结果显然要比填0更接近与原始图像,保留的信息更多。

      小结

      (1) 用一整图像进行卷积和在较大的图像上通过滑动窗提取出一个个子图象进行卷积得到的效果是相同的。

      (2) 可以这样做的主要原因在于将最后的全连接层改写成了卷积层。

      (3) 在一整张图像做卷积的效率要远远高于在图像上滑动的效率,因为前者只需要一次前向传播,而后者需要多次

      (4) 用整张图像计算与滑动窗口的方法对比,所补充的零更少(如上所讲,不用零而是用在其旁边的像素代替),提取的信息损失的更少。

      即,用整张图像直接计算卷积不仅仅在效率上高于使用滑动窗口的方法,而且更多的保留了图像的细节,完胜!

      还可以得到另一个结论在较大的图像上以步长为 2 L 2^L 2L进行滑动然后再计算的卷积结果,其效果与 以步长为 1 在大图像直接卷积得到的结果上 移动 所获得的结果 是等价的。如图5中的大红色框对应小红色框,大黄色框对应小黄色框。所以当步长为64时,将相当于以步长为2在大图的卷积结果上移动。

      对于第二个问题,如果我非要以16为步长呢?是可以的,只是这个时候所获得结果不再是如图5的那种滑动的计算方式了。还是举例说明,在上一个例子中,我们将步长32改为4,所以这里将步长16改为2进行分析。假如说原始的输入图像为一个 4×4 的图像,现在将使用一个比原来大的图像,是一个8×8的图像,使用卷积核为 4×4 大小,步长为4,则在图像进行卷积运算的如图6左侧的4个部分(红黄绿蓝),而图6 右侧的是步长为2时与原始相比增加的部分。将图 6中两个部分相加就可以得到步长为2它时所有进行卷积运算的部分了。

    这里写图片描述
    图6 .步长为4时原始图像进行卷积的部分及将步长改为2时比原来多出的部分

      获得步长为2的时进行卷积的区域。首先像之前一样对原始图像做以4为步长的卷积,这时进行卷积的部分就是图6中左侧的部分;其次将原始图片沿宽度方向平移2个像素之后,依旧进行步长为4的卷积,这个时候进行卷积的部分为图6中的红色部分和绿色部分;然后沿高度方向平移2个像素之后,按步长为4进行卷积,这个时候进行卷积的部分为图6中的蓝色部分和黄色部分;最后沿高度方向和宽度方向同时移动2个像素,按步长为4进行卷积,这个时候进行卷积的部分为图6中的紫色部分。将这些部分加在一起就是进行卷积运算你得所有区域了。

      这个结果明显是无法通过像图5中的那样滑动得到了,这样的方法所需要进行卷积的区域要远远大于以4为步长时所需要就进行卷积运算的区域;后续的卷积都是在这一卷积的结果上进行的,所以后面的都会发生改变。

      综上,步长为32的正整数倍只是保证 直接在最后的卷积结果上进行滑动和滑窗效果相等 的下限值。

    3. 卷积神经网络的结构

      卷积神经网络通常是由三种层构成:卷积层,汇聚层(除非特别说明,一般就是最大值汇聚)和全连接层(简称FC)。ReLU激活函数也应该算是是一层,它逐元素地进行激活函数操作,常常将它与卷积层看作是同一层。

    3.1 层的排列规律

      卷积神经网络最常见的形式就是将一些卷积层和ReLU层放在一起,其后紧跟汇聚层,然后重复如此直到图像在空间上被缩小到一个足够小的尺寸,在某个地方过渡成成全连接层也较为常见。最后的全连接层得到输出,比如分类评分等。换句话说,最常见的卷积神经网络结构如下:
    I N P U T → [ C O N V → R E L U ] ∗ N → [ P O O L ? ] ∗ M → [ F C → R E L U ] ∗ K → F C INPUT \rightarrow [CONV \rightarrow RELU]*N \rightarrow [POOL?]*M \rightarrow [FC \rightarrow RELU]*K \rightarrow FC INPUT[CONVRELU]N[POOL?]M[FCRELU]KFC

    其中*指的是重复次数,POOL?指的是一个可选的汇聚层。其中N >=0,通常N<=3,M>=0,K>=0,通常K<3。例如,下面是一些常见的网络结构规律:

    • INPUT -> FC ,实现一个线性分类器,此处N = M = K = 0。

    • INPUT -> CONV -> RELU -> FC,单层的卷积神经网络

    • *INPUT -> [CONV -> RELU -> POOL]2 -> FC -> RELU -> FC,此处在每个汇聚层之间有一个卷积层,这种网络就是简单的多层的卷积神经网络。

    • **INPUT -> [CONV -> RELU -> CONV -> RELU -> POOL]3 -> [FC -> RELU]2 -> FC ,此处每个汇聚层前有两个卷积层,这个思路适用于更大更深的网络(比如说这个思路就和VGG比较像),因为在执行具有破坏性的汇聚操作前,多重的卷积层可以从输入数据中学习到更多的复杂特征。

      最新进展:传统的将层按照线性进行排列的方法已经受到了挑战,挑战来自谷歌的Inception结构和微软亚洲研究院的残差网络(Residual Net)结构。这两个网络的特征更加复杂,连接结构也不同。

    3.2 卷积层的大小选择

      几个小滤波器卷积层的组合比一个大滤波器卷积层好。假设你一层一层地重叠了3个3x3的卷积层(层与层之间有非线性激活函数)。在这个排列下,第一个卷积层中的每个神经元都对输入数据体有一个3x3的视野。第二个卷积层上的神经元对第一个卷积层有一个3x3的视野,也就是对输入数据体有5x5的视野。同样,在第三个卷积层上的神经元对第二个卷积层有3x3的视野,也就是对输入数据体有7x7的视野。假设不采用这3个3x3的卷积层,而是使用一个单独的有7x7的感受野的卷积层,那么所有神经元的感受野也是7x7,但是这样有一些缺点。首先,多个卷积层与非线性的激活层交替的结构,比单一卷积层的结构更能提取出深层的更好的特征。其次,假设所有的数据有C个通道,那么单独的7x7卷积层将会包含 C × ( 7 × 7 × C ) = 49 C 2 C\times (7\times 7\times C)=49C^2 C×(7×7×C)=49C2个参数,而3个3x3的卷积层的组合仅有 3 × ( C × ( 3 × 3 × C ) ) = 27 C 2 3\times (C\times (3\times 3\times C))=27C^2 3×(C×(3×3×C))=27C2个参数。直观说来,最好选择带有小滤波器的卷积层组合,而不是用一个带有大的滤波器的卷积层。前者可以表达出输入数据中更多个强力特征,使用的参数也更少。唯一的不足是,在进行反向传播时,中间的卷积层可能会导致占用更多的内存。所以他减少的是参数量,而非计算量。

    3.3 层的尺寸设置规律

    • 输入层 ,应该能被2整除很多次。常用数字包括32(比如CIFAR-10),64,96(比如STL-10)或224(比如ImageNet卷积神经网络),384和512。

    • 卷积层 ,应该使用小尺寸滤波器(比如3x3或最多5x5),使用步长S=1。还有一点非常重要,就是对输入数据进行零填充,这样卷积层就不会改变输入数据在空间维度上的尺寸。比如,当F=3,那就使用P=1来保持输入尺寸。当F=5,P=2,一般对于任意F,当P=(F-1)/2的时候能保持输入尺寸。如果必须使用更大的滤波器尺寸(比如7x7之类),通常只用在第一个面对原始图像的卷积层上。

    • 汇聚层 ,负责对输入数据的空间维度进行降采样。最常用的设置是用用2x2感受野(即F=2)的最大值汇聚,步长为2(S=2)。注意这一操作将会把输入数据中75%的激活数据丢弃(因为对宽度和高度都进行了2的降采样)。另一个不那么常用的设置是使用3x3的感受野,步长为2。最大值汇聚的感受野尺寸很少有超过3的,因为汇聚操作过于激烈,易造成数据信息丢失,这通常会导致算法性能变差。

      上文中展示的两种设置(卷积层F=3,P=1,汇聚层F=2,P=2)是很好的,因为所有的卷积层都能保持其输入数据的空间尺寸,汇聚层只负责对数据体从空间维度进行降采样。如果使用的步长大于1并且不对卷积层的输入数据使用零填充,那么就必须非常仔细地监督输入数据体通过整个卷积神经网络结构的过程,确认所有的步长和滤波器都尺寸互相吻合,卷积神经网络的结构美妙对称地联系在一起。

      为何使用零填充?使用零填充除了前面提到的可以让卷积层的输出数据保持和输入数据在空间维度的不变,还可以提高算法性能。如果卷积层值进行卷积而不进行零填充,那么数据体的尺寸就会略微减小,那么图像边缘的信息就会过快地损失掉。

      因为内存限制所做的妥协:在某些案例(尤其是早期的卷积神经网络结构)中,基于前面的各种规则,内存的使用量迅速飙升。例如,使用64个尺寸为3x3的滤波器对224x224x3的图像进行卷积,零填充为1,得到的激活数据体尺寸是[224x224x64]。这个数量就是一千万的激活数据,或者就是72MB的内存(每张图就是这么多,激活函数和梯度都是)。因为GPU通常因为内存导致性能瓶颈,所以做出一些妥协是必须的。在实践中,人们倾向于在网络的第一个卷积层做出妥协。例如,可以妥协可能是在第一个卷积层使用步长为2,尺寸为7x7的滤波器(比如在ZFnet中)。在AlexNet中,滤波器的尺寸的11x11,步长为4。

    4. 案例学习

      下面是卷积神经网络领域中比较有名的几种结构:

    • LeNet ,第一个成功的卷积神经网络应用,是Yann LeCun在上世纪90年代实现的。当然,最著名还是被应用在识别数字和邮政编码等的LeNet结构。

    • AlexNet ,AlexNet卷积神经网络在计算机视觉领域中受到欢迎,它由Alex Krizhevsky,Ilya Sutskever和Geoff Hinton实现。AlexNet在2012年的ImageNet ILSVRC 竞赛中夺冠,性能远远超出第二名(16%的top5错误率,第二名是26%的top5错误率)。这个网络的结构和LeNet非常类似,但是更深更大,并且使用了层叠的卷积层来获取特征(之前通常是只用一个卷积层并且在其后马上跟着一个汇聚层)。

    • ZF Net ,Matthew Zeiler和Rob Fergus发明的网络在ILSVRC 2013比赛中夺冠,它被称为 ZFNet(Zeiler & Fergus Net的简称)。它通过修改结构中的超参数来实现对AlexNet的改良,具体说来就是增加了中间卷积层的尺寸让第一层的步长和滤波器尺寸更小

    • GoogLeNet ,ILSVRC 2014的胜利者是谷歌的Szeged等实现的卷积神经网络。它主要的贡献就是实现了一个奠基模块,它能够显著地减少网络中参数的数量(AlexNet中有60M,该网络中只有4M)。还有,这个论文中没有使用卷积神经网络顶部使用全连接层,而是使用了一个平均汇聚,把大量不是很重要的参数都去除掉了。GooLeNet还有几种改进的版本,最新的一个是Inception-v4

    • VGGNet ,ILSVRC 2014的第二名是Karen Simonyan和 Andrew Zisserman实现的卷积神经网络,现在称其为VGGNet。它主要的贡献是展示出网络的深度是算法优良性能的关键部分。他们最好的网络包含了16个卷积/全连接层。网络的结构非常一致,从头到尾全部使用的是3x3的卷积和2x2的汇聚。他们的预训练模型是可以在网络上获得并在Caffe中使用的。VGGNet不好的一点是它耗费更多计算资源,并且使用了更多的参数,导致更多的内存占用(140M)。其中绝大多数的参数都是来自于第一个全连接层。后来发现这些全连接层即使被去除,对于性能也没有什么影响,这样就显著降低了参数数量。

    • ResNet ,残差网络(Residual Network)是ILSVRC2015的胜利者,由何恺明等实现。它使用了特殊的跳跃链接,大量使用了批量归一化(batch normalization)。这个结构同样在最后没有使用全连接层。读者可以查看何恺明的的演讲,以及一些使用Torch重现网络的实验。ResNet当前最好的卷积神经网络模型(2016年五月)。何开明等最近的工作是对原始结构做一些优化,可以看论文Identity Mappings in Deep Residual Networks,2016年3月发表。

    4.1 VGGNet的细节

      我们进一步对VGGNet的细节进行分析学习。整个VGGNet中的卷积层都是以步长为1进行3x3的卷积,使用了1的零填充,汇聚层都是以步长为2进行了2x2的最大值汇聚。可以写出处理过程中每一步数据体尺寸的变化,然后对数据尺寸和整体权重的数量进行查看:

    INPUT: [224x224x3]        memory:  224*224*3=150K   weights: 0
    CONV3-64: [224x224x64]  memory:  224*224*64=3.2M   weights: (3*3*3)*64 = 1,728
    CONV3-64: [224x224x64]  memory:  224*224*64=3.2M   weights: (3*3*64)*64 = 36,864
    POOL2: [112x112x64]  memory:  112*112*64=800K   weights: 0
    CONV3-128: [112x112x128]  memory:  112*112*128=1.6M   weights: (3*3*64)*128 = 73,728
    CONV3-128: [112x112x128]  memory:  112*112*128=1.6M   weights: (3*3*128)*128 = 147,456
    POOL2: [56x56x128]  memory:  56*56*128=400K   weights: 0
    CONV3-256: [56x56x256]  memory:  56*56*256=800K   weights: (3*3*128)*256 = 294,912
    CONV3-256: [56x56x256]  memory:  56*56*256=800K   weights: (3*3*256)*256 = 589,824
    CONV3-256: [56x56x256]  memory:  56*56*256=800K   weights: (3*3*256)*256 = 589,824
    POOL2: [28x28x256]  memory:  28*28*256=200K   weights: 0
    CONV3-512: [28x28x512]  memory:  28*28*512=400K   weights: (3*3*256)*512 = 1,179,648
    CONV3-512: [28x28x512]  memory:  28*28*512=400K   weights: (3*3*512)*512 = 2,359,296
    CONV3-512: [28x28x512]  memory:  28*28*512=400K   weights: (3*3*512)*512 = 2,359,296
    POOL2: [14x14x512]  memory:  14*14*512=100K   weights: 0
    CONV3-512: [14x14x512]  memory:  14*14*512=100K   weights: (3*3*512)*512 = 2,359,296
    CONV3-512: [14x14x512]  memory:  14*14*512=100K   weights: (3*3*512)*512 = 2,359,296
    CONV3-512: [14x14x512]  memory:  14*14*512=100K   weights: (3*3*512)*512 = 2,359,296
    POOL2: [7x7x512]  memory:  7*7*512=25K  weights: 0
    FC: [1x1x4096]  memory:  4096  weights: 7*7*512*4096 = 102,760,448
    FC: [1x1x4096]  memory:  4096  weights: 4096*4096 = 16,777,216
    FC: [1x1x1000]  memory:  1000 weights: 4096*1000 = 4,096,000
    
    TOTAL memory: 24M * 4 bytes ~= 93MB / image (only forward! ~*2 for bwd)
    TOTAL params: 138M parameters
    

      注意,大部分的内存和计算时间都被前面的卷积层占用,大部分的参数都用在后面的全连接层,这在卷积神经网络中是比较常见的。在这个例子中,全部参数有140M,但第一个全连接层就包含了100M的参数

    5. 计算上的考量

      在构建卷积神经网络结构时,最大的瓶颈是内存瓶颈,所以如何降低内存消耗量是一个值得思考的问题。三种内存占用来源:

    • 1 来自中间数据体尺寸:卷积神经网络中的每一层中都有激活数据体的原始数值,以及损失函数对它们的梯度(和激活数据体尺寸一致)。通常,大部分激活数据都是在网络中靠前的层中(比如第一个卷积层)。在训练时,这些数据需要放在内存中,因为反向传播的时候还会用到。但是在测试时可以聪明点:让网络在测试运行时候每层都只存储当前的激活数据,然后丢弃前面层的激活数据,这样就能减少巨大的激活数据量。这实际上是底层问题,在编写框架的过程中,设计者会进行这方面的考虑。

    • 2 来自参数尺寸:即整个网络的参数的数量,在反向传播时它们的梯度值,以及使用momentum、Adagrad或RMSProp等方法进行最优化时的每一步计算缓存。因此,存储参数向量的内存通常需要在参数向量的容量基础上乘以3或者更多。

    • 3 卷积神经网络实现还有各种零散的内存占用,比如成批的训练数据,扩充的数据等等。

      一旦对于所有这些数值的数量有了一个大略估计(包含激活数据,梯度和各种杂项),数量应该转化为以GB为计量单位。把这个值乘以4,得到原始的字节数(因为每个浮点数占用4个字节,如果是双精度浮点数那就是占用8个字节),然后多次除以1024分别得到占用内存的KB,MB,最后是GB计量。如果你的网络工作得不好,一个常用的方法是降低批尺寸(batch size),因为绝大多数的内存都是被激活数据消耗掉了。

    展开全文
  • 如何你的代码得清晰整洁

    千次阅读 2015-06-18 21:22:49
    摘要:烂代码不一定是问题,只要它们没有出错,没有人会对它嗤之以鼻。但不幸的是,它们没被发现的概率太小了。...中间件,网络,可能来自你自己的公司。你知道在一个角落里有一个家伙,没有人过去管他在做什么。猜猜看
  •  ,一夜好眠。the past is already written … 句读先生、泱泱、上官 等人赞同 来答一下。(收藏的人比点赞的人多3倍是怎么回事……) 欲罢不能需要一个良好的学习情绪和专注力。我主要讲一下...
  • 本书针对上述问题提出了新颖的观点:用户并不关心产品本身有多棒,而是关心使用产品后自己有多棒。作者利用其多年的交互设计经验,生动阐释了这一观点背后的科学。可贵的是,本书并不止步于解释“为什么”,还清晰...
  • 函数、方法和接口

    千次阅读 2019-08-24 16:50:58
    1.4 函数、方法和接口 函数对应操作序列,是程序的基本组成元素。Go语言中的函数有具名和匿名之分:具名函数一般对应于包级的函数,是匿名函数的一种特例,当匿名函数引用了外部作用域中的变量时就成了闭包函数,...
  • 第六章 互联网方法论 附录 周鸿祎批注“遗失的乔布斯访谈” 我的总结 老周的互联网方法论 思维导图 周鸿祎这个人比较有争议,如果不是他,中国互联网的免费文化可能还不会像今天这样,免费,共享等等概念满天...
  • 聪明10倍的哈佛学习力,颠覆脑力,做情智双高的“少数派”! 关键词:哈佛学习力 任何你想拥有的知识和能力都是现成的,你只需要把它们复制给自己 似乎我们身边总有这样的一些人存在:他们用比别人少的时间做...
  • 统计学习方法第二版 李航【学习笔记&Python代码】

    千次阅读 多人点赞 2020-08-02 19:33:27
    以后我会在博客记录自己学习《统计学习方法》第二版这本书的笔记,其实也就是我自己认为比较重要或者有用的内容,以及部分python代码的实现。 第一章 统计学习及监督学习概论 这一章主要都是些概念,所以我可能更多...
  • 吴军的谷歌方法

    千次阅读 2019-01-05 09:41:17
    吴军的谷歌方法论|第001封信|职业天花板来自认识的局限性 企业确缺失一种新的认知,一种信息时代的思维方式——计算机思维 计算机思维的一个方面——大、小 ,对这个量级的理解 我们人类生活的环境,决定了我们对...
  • 分法原理

    千次阅读 2020-06-15 16:43:05
    原文链接: 浅谈分法原理 首发于烤羚羊的理科教室写文章浅谈变分原理烤羚羊吃老本的物理教师、民科老咸鱼784 人赞同了该文章对付数学物理中极值问题,变分大法(variational principle)可谓是杀人放火居家旅行的...
  • 分享六个程序员赚100万的方法

    千次阅读 2019-10-27 11:53:23
    为了大家更直观地了解方法,我们以故事的方式进行。下面这6个故事,你满足偷窥欲的同时,又能学到东西。 故事一:开源+流量+预判 2010 年 11 月,一个程序员做了一个决定,他把一段游戏引擎程序放到了一个...
  • 初步了解机器中浮点数表示方法

    千次阅读 2019-04-13 19:16:59
    浮点数是小数点位置变化的数,能表示的范围比定点数大很多。 比如二进制数11.11可以表示为...如果由我们自己来设计计算机,这样的确可以,不过总有人会想出更聪明的办法。首先符号位必须占一位,2可以省略不存,默...
  • 工作方法的一些总结

    千次阅读 2019-06-16 19:53:07
    工作进展缓慢的原因 无法迅速开始工作 无法专注于应该完成的工作 即使想要认真处理每一项工作,也应该狠下心来,从最为重要的工作着手,不被当时的 各种情形所左右。...◎ 虽然脑中有一些自己的意见,但出现反...
  • 谷歌是否我们得愚蠢?

    万次阅读 2014-01-21 16:32:22
    卡尔在2008年7-8月号的《大西洋月刊》撰文,以《谷歌是否我们得愚蠢》为题,痛苦地剖析了自己和互联网一代的大脑退化历程。   The Atlantic Monthly 《大西洋月刊》 JULY/AUGUST 2008 2008年7/8月号 What...
  • 第01课:高效学习方程式,你的学习到底是哪里出了问题一、学习不好的原因高效学习...第二,帮助你对照检测自己在学习上出现的问题。二、其他几个影响学习效果的因素1. 学习动机回报是学习本身:证明自己的能力、对知...
  • JAVA 面向对象之方法详解

    千次阅读 2016-07-17 23:08:29
    方法是类或对象 的行为特征的抽象. 方法是类或对象最重要的组成部分. 所有的方法都必需定义在类里. 方法不能独立存在. 方法在逻辑上要么属于类, 要么属于对象. 方法的所属性 一旦将一个方法定义在某个类的...
  • 深度学习常用方法

    千次阅读 2018-07-18 21:53:57
    Deep Learning的常用模型或者方法  1、AutoEncoder自动编码器  Deep Learning最简单的一种方法是利用人工神经网络的特点,人工神经网络(ANN)本身就是具有层次结构的系统,如果给定一个神经网络,我们假设其...
  • 听说,你想让自己的Go程序运行的更快?

    千次阅读 多人点赞 2016-04-22 15:07:57
    到现在为止,我已经忘记了我在写什么,但我确定这篇文章是关于Go语言的。这主要是一篇,关于运行速度,而不是开发速度的文章...自从使用了Go语言之后,我们已经学习到了很多提升性能以及Go在系统编程中正常运转的方法
  • 首先我们来看看 Apple 的框架。一般来说除非特别声明,大多数的类默认都不是线程安全的。对于其中的一些类来说,这是很合理的,但是对于另外一些来说就很有趣了。 就算是在经验丰富的 iOS/Mac 开发者,也难免会...
  • 快速批量去除图片水印方法大全~~

    万次阅读 2014-07-08 14:29:29
    聪明的你一定会想到,先选出跟纸一样的兰色,然后用毛笔蘸上这种兰色,在字上涂抹,直到白字完全被盖住为止。 非常正确!就应该这么干! 在Photoshop上用画笔工具去字,过程跟这非常相似!只不过在“选”完需要的...
  • 近期杂文集锦-数学方法

    千次阅读 2017-08-05 09:47:22
    也就是说,笛卡尔提出了一套方法论,从此以后,只要照着做就能得到答案,再也不需要聪明的才智和神点的灵感(这话有点夸张,只是想表达我对笛卡尔方法的惊叹),换句话说,笛卡尔把几何学给大众化了,这是其伟大之处。...
  • 我们看看这个恶意合约是如何利用我们的  EtherStore  合约的。攻击者可以(假定恶意合约地址为  0x0...123  )使用  EtherStore  合约地址作为构造函数参数来创建上述合约。这将初始化并将公共变量  ...
  • 架构制图:工具与方法

    千次阅读 2020-09-28 13:50:25
    一方面,人类的思维能力终归有限,必须依靠架构这种高度抽象和简化的蓝图,才能复杂系统的创造、理解、分析和治理得可行;另一方面,量级达到一定程度的大型系统,也只能依靠多人分工合作才能完成,而架构也正是...
  • 软件工程 实践者的研究方法 中文题答案

    千次阅读 多人点赞 2021-01-27 23:26:33
    19.1用自己的话,描述为什么在面向对象系统中,类是最小的合理测试单元。 答:在面向对象软件中,单元的概念发生了变化,不再是传统软件单元测试中关注的算 法细节和流经模块接口数据,而是测试由封装在类中的操作...
  • 提升自己的认知-思维模型

    千次阅读 2018-05-06 20:29:25
    最近看到如何判断一个人是杰出的聪明人还是平庸的普通人?的文章,感触颇深。现摘录有所感触的思维方式。 一、思维模型 1、每周“5小时”学习法则 参考:https://36kr.com/p/5130501.html?from=authorlatest A...
  • 相信不仅是我自己在阅读和总结“实时渲染的圣经”《Real-Time Rendering 3rd》的过程中受益良多,一路阅读这个系列文章的朋友们,也应该是颇有收获。 感谢大家一直以来的支持与陪伴。 导读 这篇文章约1万8千字,构成...
  • apply()方法

    万次阅读 2018-11-01 16:37:32
    作用:apply( ) 方法调用一个具有给定this值的函数,以及作为一个数组(或类似数组对象)提供的参数。 参数 : 一共有2个参数,均为可选参数 第一个参数: this指向。(this可能不是该方法看到的实际值:如果这个...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 68,489
精华内容 27,395
关键字:

如何让自己变聪明的方法