本文介绍如何使用 .NET MAUI 开发一个电子木鱼应用。以实际的小应用开发为例,通过这个开发过程,介绍了其涉及的 .NET MAUI、Blazor、前端等相关知识点。文章涉及的应用已开源在 Github,大家可前往下载体验: https://github.com/sangyuxiaowu/MuYu
文章目录
电子木鱼不知道从什么时候火了起来,成了年轻人的新时尚。年轻人没有选择经常去寺庙像和尚那样念经,而是下载了电子木鱼软件,进行线上敲木鱼。虽然搞不懂这种“敲电子木鱼,见机甲佛祖,积赛博功德,修图灵真经”的赛博玄学,但是这个软件它做起来容易,需求还简单。在这么焦虑的假期生活中,还是假期的最后一天,最适合玩一玩 .NET MAUI 了。

这个项目及文章使用并介绍了一下相关知识点:
界面 UI 方面因为使用还是 Web 的技术,所以更多的是前端的知识,可自行探索。
这个电子木鱼整体功能非常简单,主要就是点击一下屏幕播放一下木鱼敲击的音频,这个是核心功能。
再往下设计,就是一些动效的优化,比如:
更进一层的设计:
再接下来迭代,当然也是有很多参考的相关案例,比如:
当然,以上只是瞎扯,作为一个清心寡欲的木鱼,做好自己定位是第一步。“天之道,不争而善胜”,作为佛系木鱼,当然只是纯粹的敲击,满足最基本的需求就可以了。
这个开发第一步,沿用我之前讲的那个《MAUI 安卓 UI 资源设置》文章的内容,先把 UI 资源处理一下。这次被老婆拒绝了,我亲自拿钢笔工具把这个木鱼 Logo 给绘制出来了,作为一个合格的全栈,美工也是需要会的。

这个 Logo 画完,内部应用的主体界面木鱼自然也就设计好了。
电子木鱼的核心功能,当然是点击屏幕播放一下敲击音效了。因为我们使用的是 .NET MAUI Blazor ,为了后续界面动效的设计,我们通过前端的方式来播放音乐。
为了图方便,直接在 wwwroot/index.html HTML 入口文件中添加如下 audio 标签和 JS 方法:
<audio id="player">
<source src="other/muyu.wav" />
</audio>
<script>
window.PlayAudioFile = (tips) => {
var audio = document.getElementById('player');
audio.play();
}
</script>
在应用的首页添加图片点击的事件,来调用 JS,并预留了文字浮动内容的传递:
<img id="img" src="/images/muyu.svg" style="height:100%;max-width:100%" @onclick="ClickHandler" />
async Task ClickHandler()
{
await JS.InvokeAsync<string>("PlayAudioFile", tipsInfo);
}
本小节相关知识点:《JavaScript 互操作》
动效设计这里我们主要实现木鱼敲击放大动效和敲击后的文字浮动。这里主要是前端的技术,我们使用 CSS3 的 @keyframes 创建两个动画规则:
@keyframes showbig {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
@keyframes showtips {
0% {
opacity: 1;
transform: translateY(0);
}
75% {
opacity: .9;
transform: translateY(-1.5em);
}
100% {
opacity: 0;
transform: translateY(-2em);
}
}
这里两个动效都是默认是只循环一遍,时长 0.3s :
.showbig {
animation-timing-function: ease-in-out;
animation-duration: .3s;
animation-name: showbig;
}
通过 JS 方式控制 DOM 的 Class 添加删除来实现动画的播放:
function ShowBig() {
var img = document.getElementById('img');
img.classList.add('showbig');
setTimeout(() => {
img.classList.remove('showbig');
}, 300);
}
对于浮动的文字来说,快速敲击时,为了可以显示多条,这边的浮动文字是动态创建的 P 标签。还记得前面预留的 tipsInfo 吧,是在这里使用的。
function ShowTips(text) {
var p = document.createElement("p");
p.classList.add('tips');
p.innerHTML = text;
var tips = document.getElementById('tips');
tips.appendChild(p);
setTimeout(() => {
tips.removeChild(p)
}, 300);
}
有了设置当然就可以搞个菜单出来了,菜单这里的设计也非常简单,我们可以直接新建一个 Blazor 组件 Shared/SettingMenu.razor,来实现传递菜单项来生成菜单组件,并在点击选项后返回点击的菜单序号。
<div class="mask" style="display:@(Show?"flex":"none")">
<div class="setting_menu">
<div class="title">@MenuTitle</div>
<ul class="list">
@for (int i=0;i<NavData.Count(); i++)
{
<li @onclick="e=>MenuClick(i)">@NavData[i]</li>
}
</ul>
</div>
</div>
/// <summary>
/// 点击回调
/// </summary>
[Parameter]
public EventCallback<int> OnMenuClick { get; set; }
/// <summary>
/// 菜单标题
/// </summary>
[Parameter]
public string MenuTitle { get; set; } = "设置";
/// <summary>
/// 显示隐藏
/// </summary>
[Parameter]
public bool Show { get; set; } = false;
/// <summary>
/// 菜单信息
/// </summary>
[Parameter]
public string[] NavData { get; set; }
/// <summary>
/// 通知点击事件
/// </summary>
/// <param name="inx">点击的index</param>
/// <returns></returns>
async Task MenuClick(int inx)
{
await OnMenuClick.InvokeAsync(inx);
}
那,跑一下看看,似乎,大概,也许是非常完美:

在我们的悬浮文字设置选项中,需要提供输入功能。当然,直接前端的方案,设计一个模态对话框似乎是一个非常好的方案,也符合 UI 的一致性。但是考虑到能省则省,这边没有使用前端的框架,所以再手写一个会稍微费点事的。如果说省事,当然是直接前端 JS 的 prompt ,但是这样又显得格格不入:

.NET MAUI Blazor 这种混合开发的模式大多都一样,对话框也是一个小问题。不过这里可以非常方便的使用本机对话框,只要拿到 Page 即可,在 razor 文件的 code 代码块中,我们可以这样用:
string result = await Application.Current.MainPage.DisplayPromptAsync("悬浮文字设置", "请设置敲击后的自定义悬浮文字", "确定", "取消", "请输入悬浮文字", 5, null, tipsInfo);
本小节相关知识点:《显示弹出窗口》
搞定了设置文字的交互,接下来就是存储了,总体来说我们需要存储的木鱼的总敲击次数,今日敲击次数和刚刚的自定义悬浮文字。毕竟不是复杂的应用,就没必要上数据库了,对于这种简单的键值数据存储我们可以直接选用 Preferences。Preferences 通过调用 Preferences.Set 方法来设置,提供键和值,以下就是完整的设置悬浮文字和存储的方法:
/// <summary>
/// 获取初始的悬浮文字
/// </summary>
private string tipsInfo = Preferences.Default.Get("tips", "功德+1");
async Task SetTips()
{
string result = await Application.Current.MainPage.DisplayPromptAsync("悬浮文字设置", "请设置敲击后的自定义悬浮文字", "确定", "取消", "请输入悬浮文字", 5, null, tipsInfo);
if (string.IsNullOrWhiteSpace(result)) return;
Preferences.Default.Set("tips", result);
tipsInfo = result;
}
本小节相关知识点:《Preferences》
做的差不多了,菜单也有了,可以简单跑一下看看了,但是似乎菜单出了些问题,设置菜单的按钮并不好用,每一个回调都是 4:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9ywko3Od-1675503302385)(./img/2.png)]
啊对,前面的完美,我是说界面。BUG 总会有的,还总是发生在不经意之间。
元芳你怎么看?
元芳:问一问 ChatGPT 吧,他说他懂这个的。
ChatGPT:是的,我了解Blazor。Blazor是一种用于开发Web应用程序的框架,允许使用C#代码和.NET运行时在浏览器中运行Web应用程序。它提供了一种方便的方法来使用.NET技术来构建客户端Web应用程序,而无需学习JavaScript。
仔细检查我们会发现,在前面的 SettingMenu.razor 菜单组件中,我们有一个循环,通过枚举NavData数组中的每一项并创建一个包含该项的列表项。因为需要序号,所以这里用的 for 循环,问题在于,在回调函数 MenuClick(i) 中,变量i是局部的,在循环结束后其值就改变了。
为了解决这个问题,可以将当前项的索引存储在闭包中:
<ul class="list">
@for (int i = 0; i < NavData.Count(); i++)
{
int index = i;
<li @onclick="e => MenuClick(index)">@NavData[i]</li>
}
</ul>
修改完代码,再次运行:

至此,这个简单的电子木鱼的基础功能已实现,应用已经完成了大半。对于未尽的事宜,比如敲击计数和其他设置的功能我下次再说,容我先去敲一会木鱼,静个心。

我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当
我已经在Sinatra上创建了应用程序,它代表了一个简单的API。我想在生产和开发上进行部署。我想在部署时选择,是开发还是生产,一些方法的逻辑应该改变,这取决于部署类型。是否有任何想法,如何完成以及解决此问题的一些示例。例子:我有代码get'/api/test'doreturn"Itisdev"end但是在部署到生产环境之后我想在运行/api/test之后看到ItisPROD如何实现? 最佳答案 根据SinatraDocumentation:EnvironmentscanbesetthroughtheRACK_ENVenvironm
我们的git存储库中目前有一个Gemfile。但是,有一个gem我只在我的环境中本地使用(我的团队不使用它)。为了使用它,我必须将它添加到我们的Gemfile中,但每次我checkout到我们的master/dev主分支时,由于与跟踪的gemfile冲突,我必须删除它。我想要的是类似Gemfile.local的东西,它将继承从Gemfile导入的gems,但也允许在那里导入新的gems以供使用只有我的机器。此文件将在.gitignore中被忽略。这可能吗? 最佳答案 设置BUNDLE_GEMFILE环境变量:BUNDLE_GEMFI
这似乎非常适得其反,因为太多的gem会在window上破裂。我一直在处理很多mysql和ruby-mysqlgem问题(gem本身发生段错误,一个名为UnixSocket的类显然在Windows机器上不能正常工作,等等)。我只是在浪费时间吗?我应该转向不同的脚本语言吗? 最佳答案 我在Windows上使用Ruby的经验很少,但是当我开始使用Ruby时,我是在Windows上,我的总体印象是它不是Windows原生系统。因此,在主要使用Windows多年之后,开始使用Ruby促使我切换回原来的系统Unix,这次是Linux。Rub
我正在玩HTML5视频并且在ERB中有以下片段:mp4视频从在我的开发环境中运行的服务器很好地流式传输到chrome。然而firefox显示带有海报图像的视频播放器,但带有一个大X。问题似乎是mongrel不确定ogv扩展的mime类型,并且只返回text/plain,如curl所示:$curl-Ihttp://0.0.0.0:3000/pr6.ogvHTTP/1.1200OKConnection:closeDate:Mon,19Apr201012:33:50GMTLast-Modified:Sun,18Apr201012:46:07GMTContent-Type:text/plain
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList()Obt
@作者:SYFStrive @博客首页:HomePage📜:微信小程序📌:个人社区(欢迎大佬们加入)👉:社区链接🔗📌:觉得文章不错可以点点关注👉:专栏连接🔗💃:感谢支持,学累了可以先看小段由小胖给大家带来的街舞👉微信小程序(🔥)目录自定义组件-behaviors 1、什么是behaviors 2、behaviors的工作方式 3、创建behavior 4、导入并使用behavior 5、behavior中所有可用的节点 6、同名字段的覆盖和组合规则总结最后自定义组件-behaviors 1、什么是behaviorsbehaviors是小程序中,用于实现
了解Rails缓存如何工作的人可以真正帮助我。这是嵌套在Rails::Initializer.runblock中的代码:config.after_initializedoSomeClass.const_set'SOME_CONST','SOME_VAL'end现在,如果我运行script/server并发出请求,一切都很好。然而,在我的Rails应用程序的第二个请求中,一切都因单元化常量错误而变得糟糕。在生产模式下,我可以成功发出第二个请求,这意味着常量仍然存在。我已通过将以上内容更改为以下内容来解决问题:config.after_initializedorequire'some_cl
我有一个使用PDFKit呈现网页的pdf版本的Rails应用程序。我使用Thin作为开发服务器。问题是当我处于开发模式时。当我使用“bundleexecrailss”启动我的服务器并尝试呈现任何PDF时,整个过程会陷入僵局,因为当您呈现PDF时,会向服务器请求一些额外的资源,如图像和css,看起来只有一个线程.如何配置Rails开发服务器以运行多个工作线程?非常感谢。 最佳答案 我找到的最简单的解决方案是unicorn.geminstallunicorn创建一个unicorn.conf:worker_processes3然后使用它: