【TopDesk】3.1.1. 利用IMMNotificationClient实现耳机插拔检测
鉴于本人并非Win32专精,C++也只是学了个大概,因此涉及到底层的部分或有疏漏之处,拙文献丑,还请各位道友多多包涵体谅,能提出修改意见更是感激不尽。
0x00 现代化探索
早在以前写点GDI小程序的时候,巨硬同志就给我留下了这样的不良印象:
Windows很乱复杂,为windows写程序也很乱很复杂。
这导致了我很长一段时间都不敢碰C++,装了VS Community好久一次都没打开过,能用Java+写好的库就用。
可惜这种曲线救国的路终究走不长久,Stackoverflow大法也终究有不管用的时候。
由于早期的电脑设计是一个高度模块化的系统,声音处理被分为了单独的一块,理论上不管是耳机还是扬声器都是由声卡直接管理的。当然现在的声卡(日常笔记本)大都是板载的集成声卡,而且windows上也有Audio Endpoint Device (音频终端设备)这一概念,也不是完全没有路可以走。
然而只要打开过windows设备管理器的人都知道,耳机是没有作为一个设备出现在设备列表里面的。实际上在控制面板-声音中显示的音频输出并非扬声器或者耳机,而是声卡自己配置的音频输出通道,这也印证了“声卡直接接管音频IO的工作”。
(板载声卡是Conexant SmartAudio HD,其他比如Realtek家的估计也差不多,无法验证,欢迎补充)
接下来的这段是关于音频终端的设备的探索,由于对这一块的工作模式没什么了解,因此熟悉的读者可以直接跳过。
所幸打开Conexant SmartAudio HD的驱动详细信息可以看到,在集成声卡的下一层是\Driver\HDAudBus
(见下图),度娘告诉我们这个东西的全名是Microsoft UAA Bus Driver for High Definition Audio
(针对HD音频的微软UAA总线驱动),而这里的UAA
则指的是Universal Audio Architecture (通用音频架构)。这是一个好消息,因为UAA标准是微软自己提出来的,也就意味着我们很可能可以通过windows API直接访问到上面的设备列表。
同样详细信息中的“总线关系”项提供了新的线索:
总线之下的设备包括两项,分别叫做SWD\MMDEVAPI\{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}
和SWD\MMDEVAPI\{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}
,猜测两个分别是扬声器与耳机接口(后来被证实),而这个MMDEVAPI
则是指的Windows Multimedia Device API (Windows多媒体设备API)。这个线索将我们引到了一个真正的突破:IMMNotificationClient。
0x01 三面红旗
有人说程序员有了一定境界的标志就是,能提出一个没有人研究过的问题。
IMMNotificationClient是何方神圣呢?英语好的道友大概能看出来,它能够提供一个通知设备改变事件的监听器。
事实也确实如此。
通过继承这个类,可以监听到音频终端设备的新增(OnDeviceAdded)、移除(OnDeviceRemoved)、状态更改(OnDeviceStateChanged)、属性值更改(OnPropertyValueChanged)以及默认设备更改(OnDefaultDeviceChanged)。这些方法的定义与用法在MSDN上面都有介绍,在此不多赘述了。
Talk is cheap, show me the code,什么都比不上一个热腾腾的Demo来的有用。
根据这两篇文章(1,2)写了一个简单的示例,代码如下:
(基本纯复制/粘贴,代码很乱,iostream和stdio一起用不能忍)
#define SAFE_RELEASE(punk) \if ((punk) != NULL) \{ (punk)->Release(); (punk) = NULL; } #include <windows.h>
#include <setupapi.h>
#include <initguid.h>
#include <mmdeviceapi.h>
#include <Functiondiscoverykeys_devpkey.h>
#include "iostream"
#include <stdio.h>
using namespace std; class CMMNotificationClient : public IMMNotificationClient
{
public: IMMDeviceEnumerator *m_pEnumerator; CMMNotificationClient(): _cRef(1), m_pEnumerator(NULL) { // 初始化COM ::CoInitialize(NULL); HRESULT hr = S_OK; // 创建接口hr = CoCreateInstance( __uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&m_pEnumerator); if (hr==S_OK) { cout<<"接口创建成功"<<endl; } else { cout<<"接口创建失败"<<endl; } // 注册事件hr = m_pEnumerator->RegisterEndpointNotificationCallback((IMMNotificationClient*)this); if (hr==S_OK){ cout<<"注册成功"<<endl; } else { cout<<"注册失败"<<endl; } } ~CMMNotificationClient() { SAFE_RELEASE(m_pEnumerator) ::CoUninitialize(); } // IUnknown methods -- AddRef, Release, and QueryInterface
private: LONG _cRef; // Private function to print device-friendly nameHRESULT _PrintDeviceName(LPCWSTR pwstrId);ULONG STDMETHODCALLTYPE AddRef() { return InterlockedIncrement(&_cRef); } ULONG STDMETHODCALLTYPE Release() { ULONG ulRef = InterlockedDecrement(&_cRef); if (0 == ulRef) { delete this; } return ulRef; } HRESULT STDMETHODCALLTYPE QueryInterface( REFIID riid, VOID **ppvInterface) { if (IID_IUnknown == riid) { AddRef(); *ppvInterface = (IUnknown*)this; } else if (__uuidof(IMMNotificationClient) == riid) { AddRef(); *ppvInterface = (IMMNotificationClient*)this; } else { *ppvInterface = NULL; return E_NOINTERFACE; } return S_OK; } HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged( EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) { cout<<"OnDefaultDeviceChanged"<<endl;return S_OK; } HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId) { cout<<"OnDeviceAdded"<<endl;return S_OK; }; HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId) { cout<<"OnDeviceRemoved"<<endl;return S_OK; } HRESULT STDMETHODCALLTYPE OnDeviceStateChanged( LPCWSTR pwstrDeviceId, DWORD dwNewState) { cout<<"OnDeviceStateChanged"<<endl;return S_OK; } HRESULT STDMETHODCALLTYPE OnPropertyValueChanged( LPCWSTR pwstrDeviceId, const PROPERTYKEY key) { cout<<"OnPropertyValueChanged"<<endl;_PrintDeviceName(pwstrDeviceId);return S_OK; }
}; // Given an endpoint ID string, print the friendly device name.
HRESULT CMMNotificationClient::_PrintDeviceName(LPCWSTR pwstrId)
{HRESULT hr = S_OK;IMMDevice *pDevice = NULL;IPropertyStore *pProps = NULL;PROPVARIANT varString;CoInitialize(NULL);PropVariantInit(&varString);if (m_pEnumerator == NULL){// Get enumerator for audio endpoint devices.hr = CoCreateInstance(__uuidof(MMDeviceEnumerator),NULL, CLSCTX_INPROC_SERVER,__uuidof(IMMDeviceEnumerator),(void**)&m_pEnumerator);}if (hr == S_OK){hr = m_pEnumerator->GetDevice(pwstrId, &pDevice);}if (hr == S_OK){hr = pDevice->OpenPropertyStore(STGM_READ, &pProps);}if (hr == S_OK){// Get the endpoint device's friendly-name property.hr = pProps->GetValue(PKEY_Device_FriendlyName, &varString);}printf("----------------------\nDevice name: \"%S\"\n"" Endpoint ID string: \"%S\"\n",(hr == S_OK) ? varString.pwszVal : L"null device",(pwstrId != NULL) ? pwstrId : L"null ID");PropVariantClear(&varString);SAFE_RELEASE(pProps)SAFE_RELEASE(pDevice)return hr;
}int main(int argc, TCHAR* argv[], TCHAR* envp[])
{ CMMNotificationClient mmClient;cin.get();return 0;
}
注:一开始导入windows.h、setupapi.h、initguid.h是因为_PrintDeviceName
里面用的PKEY_Device_FriendlyName
常量需要。理论上这个常量只要直接导入Functiondiscoverykeys_devpkey.h就可以了,不过可能因为Dev C++自带的MinGW版本问题,不加前面这三个会报undefined reference to 'PKEY_Device_FriendlyName'
,就按网上的解决方法加上了,各位有更好的欢迎提出。
编译运行结果如下,其间插拔两次耳机:
(为阅读效果做了部分处理,以//
开头的行是注释)
接口创建成功
注册成功//第一次插入耳机OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"// 注意这里是External
OnPropertyValueChanged
----------------------
Device name: "External Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"
OnPropertyValueChanged
----------------------
Device name: "External Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"
OnPropertyValueChanged
----------------------
Device name: "External Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"
OnPropertyValueChanged
----------------------
Device name: "External Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"
OnPropertyValueChanged
----------------------
Device name: "External Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"
OnPropertyValueChanged
----------------------
Device name: "External Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"// 第一次拔出耳机
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"// 注意这里是Internal
OnPropertyValueChanged
----------------------
Device name: "Internal Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"
OnPropertyValueChanged
----------------------
Device name: "Internal Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"
OnPropertyValueChanged
----------------------
Device name: "Internal Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"
OnPropertyValueChanged
----------------------
Device name: "Internal Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"
OnPropertyValueChanged
----------------------
Device name: "Internal Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"
OnPropertyValueChanged
----------------------
Device name: "Internal Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"// 第二次插入耳机,输出内容与第一次基本相同
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: "External Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"
OnPropertyValueChanged
----------------------
Device name: "External Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"
OnPropertyValueChanged
----------------------
Device name: "External Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"
OnPropertyValueChanged
----------------------
Device name: "External Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"
OnPropertyValueChanged
----------------------
Device name: "External Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"
OnPropertyValueChanged
----------------------
Device name: "External Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"// 第二次拔出耳机
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: ""
Endpoint ID string: "{0.0.0.00000000}.{c3efdd8a-e470-4841-9617-a42320dd6041}"
OnPropertyValueChanged
----------------------
Device name: "Internal Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"
OnPropertyValueChanged
----------------------
Device name: "Internal Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"
OnPropertyValueChanged
----------------------
Device name: "Internal Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"
OnPropertyValueChanged
----------------------
Device name: "Internal Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"
OnPropertyValueChanged
----------------------
Device name: "Internal Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"
OnPropertyValueChanged
----------------------
Device name: "Internal Microphone (Conexant SmartAudio HD)"
Endpoint ID string: "{0.0.1.00000000}.{272497f4-319c-4cdd-a2ce-6288c70ebe0a}"
很可惜的是本来期望会有的反映的OnDeviceAdded/OnDeviceRemoved并没有输出,耳机插拔只在OnProperyValueChanged里面有所反映,而且唯一的不同点只在于设备名称以External
还是Internal
开头。
不过好消息是,这样作为检测耳机插拔就基本上够了。使用IMMNotificationClient在理论上是可行的。
另外就是,这也验证了我们前面的猜想:驱动详细信息中的总线关系的两个子项正是我们的扬声器和耳机——输出的Endpoint ID string和前面的值完全相同。
至于具体怎么实现,又怎么与Java代码相集成(见总起篇),由于篇幅所限,我们下篇文章不见不散。
【TopDesk】3.1.1. 利用IMMNotificationClient实现耳机插拔检测相关推荐
- WM8960耳机插拔检测
WM8960支持耳机插拔检测功能,其中ADCLRC/GPIO1.LINPUT3/JD2和RINPUT3/JD3可以用来作为耳机插拔检测引脚. 需要配置的寄存器有如下几个: 1.R24的5.6位.第6位 ...
- Android4.×耳机插拔检测
Android4.2耳机插拔检测实现方法 1. 耳机检测的硬件原理 一般的耳机检测包含普通的耳机检测和带mic的耳机检测两种,这两种耳机统称为Headset,而对于不带mic的耳机,一般称之为Head ...
- linux耳机插拔检测,Android应用开发之耳机插拔处理两种方式
本文将带你了解Android应用开发[RK3288][Android6.0] 耳机插拔处理两种方式,希望本文对大家学Android有所帮助. [RK3288][Android6.0] 耳机插拔处理 ...
- 高通平台耳机插拔检测
https://blog.csdn.net/u012899335/article/details/82312766 高通耳机的插拔检测需要配置NC或NO,并且使用匹配的耳机(欧标,美标). 欧标,美标 ...
- 【audio】耳机插拔 线控按键识别流程【转】
耳机插拔/线控按键识别流程 耳机插拔/线控按键识别流程 1.文档概述 本文以msm8909平台,android N为例,介绍了通用情况下,耳机插拔的流程步骤,以及对耳机类型的识别逻辑.以方便在项目工作 ...
- 【audio】耳机插拔/线控按键识别流程
耳机插拔/线控按键识别流程 1.文档概述 本文以msm8909平台,android N为例,介绍了通用情况下,耳机插拔的流程步骤,以及对耳机类型的识别逻辑.以方便在项目工作中经常会遇到耳机不被识别,或 ...
- Android耳机耳机,Android 耳机插拔流程源码跟踪浅析
Android 开发过程中,使用耳机控制拍照,控制音乐播放,控制打电话等,线控再到蓝牙控... 耳机也在不断升级,耳机插拔的程序这一块也在不断完善.因此,在定制开发过程中,阅读这部分流程代码是必修的功 ...
- AVPlayer耳机插拔
AVPlayer耳机插拔暂停播放. //耳机插拔监听 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector ...
- CoreAudioApi-音频端点设备-检测耳机插拔
术语"端点设备"是指位于数据路径一端的硬件设备,该数据路径源自或终止于应用程序.音频终端设备的例子有扬声器.耳机.麦克风和CD播放器.沿着数据路径移动的音频数据可能在应用程序和端点 ...
最新文章
- ASP.NET中实现打印
- 电感计算软件_一文让你了解到共模电感和差模电感的差异
- 老铁666,快手突然“快”不动了?
- Linux安装Kafka-manager可视化
- 字符之间或者结构体之间比较
- Android之如何解决Android Studio左边的的project不见了
- jdk11换jdk8版本_在JDK 9(以及8)以及更高版本中,所有内容都可以作为一个流
- 华为鸿蒙与佳华,华为鸿蒙系统发布,带来三大好消息
- Android 应用开发(37)---RelativeLayout(相对布局)
- PAT甲级1024 ASCII码与整数转换
- ant基本命令和使用
- 对讲机在哪插卡?插卡对讲机是什么意思呢?5000公里对讲机的哪点事
- 解决接收 ACTION_PACKAGE_REPLACED 的广播会另外接收到 REMOVED 和 ADDED 的问题
- 欢迎来怼--第三十六次Scrum会议
- 前端--CSS选择器,盒子模型学习
- 利用Python turtle库制作夜空
- TensorFlow2学习七、使用MNIST手写体识别数据集识别自己手写图片
- iOS客户端的title不显示解决方案
- Web入门_朽木|学习笔记之第一章-数据库基本知识(1.1-1.7)
- 【浏览器插件推荐】Bookmarks clean up清除重复、废弃收藏夹