草庐IT

AppWidget(桌面小部件)

眼中有码 2023-03-28 原文

一、引言

最近开始准备做车机的 Launcher ,之前没接触过Launcher最近开始恶补这块知识,在学习Launcher3 的过程中发现了一个很有趣的东西那就 AppWidget(桌面小部件),
并且在我们项目规划的Launcher 中AppWidget占了很大的比重,所以学习好AppWidget至关重要。

二、AppWidget简介

  • Android widget 也称为桌面插件,其是android系统应用开发层面的一部分,但是又有特殊用途,而且会成为整个android系统的亮点。Android中的AppWidget与google widget和中移动的widget并不是一个概念,这里的AppWidget只是把一个进程的控件嵌入到别外一个进程的窗口里的一种方法。
  • AppWidget的服务核心在AppWidgetService中,它是系统应用,在SystemServer进程中。
  • AppWidget的提供方由应用提供(对大部分应用开发者来说,了解操作这一块就够了)。
  • AppWidget的显示方,基本上运行在Launcher中。
  • AppWidget支持的控件是由局限性的,比如不支持RecyclerView等。
  • RemoteViews 在Android中的使用场景主要有:自定义通知栏和桌面小部件。

如下图红色箭头所指的都是 AppWidget


image.png

三、Launcher3 AppWidget的启动添加流程

1. Launcher3启动添加

Launcher启动onCreate()方法初始化mAppWidgetManager, mAppWidgetHost对象,AppWidgetHost是launcher承载AppWidgetView的宿主。

public void onCreate() {
    ...
    //得到AppWidget管理实例 : AppWidgetManager , AppWidgetHost , AppWidgetHostView三个类的关系
    mAppWidgetManager = AppWidgetManagerCompat.getInstance(this);  //1
    mAppWidgetHost = new LauncherAppWidgetHost(this);  //2
    // Host启动监听,监听LauncherProvider中的数据改变
    mAppWidgetHost.startListening();  //3
    ...
}
  1. AppWidgetManagerCompat 管理类是一个单例模式的兼容类
    public static AppWidgetManagerCompat getInstance(Context context) {
        synchronized (sInstanceLock) {
            if (sInstance == null) {
                if (Utilities.ATLEAST_OREO) {
                    sInstance = new AppWidgetManagerCompatVO(context.getApplicationContext());
                } else {
                    sInstance = new AppWidgetManagerCompatVL(context.getApplicationContext());
                }
            }
            return sInstance;
        }
    }
  1. LauncherAppWidgetHost extends AppWidgetHost 由其父类完成初始化对象,创建用于回调的Callbacks服务类IAppWidgetHost.Stub, 绑定服务bindService,得到IAppWidgetService对象,进行launcher和AppWidgetService之间的调用
 public AppWidgetHost(Context context, int hostId, OnClickHandler handler, Looper looper) {
        mContextOpPackageName = context.getOpPackageName();
        mHostId = hostId;
        mOnClickHandler = handler;
        mHandler = new UpdateHandler(looper);
        mCallbacks = new Callbacks(mHandler);
        mDisplayMetrics = context.getResources().getDisplayMetrics();
        bindService(context);
    }

    private static void bindService(Context context) {
        synchronized (sServiceLock) {
            if (sServiceInitialized) {
                return;
            }
            sServiceInitialized = true;
            PackageManager packageManager = context.getPackageManager();
            if (!packageManager.hasSystemFeature(PackageManager.FEATURE_APP_WIDGETS)
                    && !context.getResources().getBoolean(R.bool.config_enableAppWidgetService)) {
                return;
            }
            IBinder b = ServiceManager.getService(Context.APPWIDGET_SERVICE);
            sService = IAppWidgetService.Stub.asInterface(b);
        }
    }
  1. 在startListening 方法中 ,通过IAppWidgetService.startListening 方法解析Launcher中的AppWidget信息保存到系统服务成员变量中。
  public void startListening() {
        if (sService == null) {
            return;
        }
        final int[] idsToUpdate;
        synchronized (mViews) {
            int N = mViews.size();
            idsToUpdate = new int[N];
            for (int i = 0; i < N; i++) {
                idsToUpdate[i] = mViews.keyAt(i);
            }
        }
        List<PendingHostUpdate> updates;
        try {
            updates = sService.startListening(
                    mCallbacks, mContextOpPackageName, mHostId, idsToUpdate).getList();
        }
        catch (RemoteException e) {
            throw new RuntimeException("system server dead?", e);
        }

        int N = updates.size();
        for (int i = 0; i < N; i++) {
            PendingHostUpdate update = updates.get(i);
            switch (update.type) {
                case PendingHostUpdate.TYPE_VIEWS_UPDATE:
                    updateAppWidgetView(update.appWidgetId, update.views);
                    break;
                case PendingHostUpdate.TYPE_PROVIDER_CHANGED:
                    onProviderChanged(update.appWidgetId, update.widgetInfo);
                    break;
                case PendingHostUpdate.TYPE_VIEW_DATA_CHANGED:
                    viewDataChanged(update.appWidgetId, update.viewId);
            }
        }
    }
  1. 当添加AppWidget时,首页返回到Launcher中的onActivityResult中,在handleActivityResult中创建添加小部件意图,之后返回到onActivityForResult,调用completeAddAppWidget,通过IAppWidgetService.getAppWidgetInfo,获取AppWidgetProviderInfo,保存到本地数据库中addItemToDatabase(),并创建AppWidgetHostView 对象,mAppWidgetHost.createView,返回RemoteView对象,IAppWidgetService。getAppWidgetViews(),调用AppWidgetHostView.updateAppWidget(views);更新View到launcher界面上mWorkspace.addInScreen(hostView, launcherInfo);
  @Thunk void completeAddAppWidget(int appWidgetId, ItemInfo itemInfo,
            AppWidgetHostView hostView, LauncherAppWidgetProviderInfo appWidgetInfo) {

        if (appWidgetInfo == null) {
            appWidgetInfo = mAppWidgetManager.getLauncherAppWidgetInfo(appWidgetId);
        }

        LauncherAppWidgetInfo launcherInfo;
        launcherInfo = new LauncherAppWidgetInfo(appWidgetId, appWidgetInfo.provider);
        launcherInfo.spanX = itemInfo.spanX;
        launcherInfo.spanY = itemInfo.spanY;
        launcherInfo.minSpanX = itemInfo.minSpanX;
        launcherInfo.minSpanY = itemInfo.minSpanY;
        launcherInfo.user = appWidgetInfo.getProfile();

        getModelWriter().addItemToDatabase(launcherInfo,
                itemInfo.container, itemInfo.screenId, itemInfo.cellX, itemInfo.cellY);

        if (hostView == null) {
            // Perform actual inflation because we're live
            hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo);
        }
        hostView.setVisibility(View.VISIBLE);
        prepareAppWidget(hostView, launcherInfo);
        mWorkspace.addInScreen(hostView, launcherInfo);
    }
  1. 当AppWidgetProvider获得更新的广播,并执行onUpdate(),onUpdate()中创建了RemoteViews并通过AppWidgetManager.updateAppWidget()更新到AppWidgetService之后,AppWidgetService会通过注册的IAppWidgetHost的回调,执行AppWidgetHost的更新。

2. Lancher3 预置 AppWidget

  • 添加权限
    <uses-permission android:name="android.permission.BIND_APPWIDGET"" />
  • 在res/xml/default_workspace_4x4.xml、default_workspace_5x5.xml等中添加
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2009 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at

          http://www.apache.org/licenses/LICENSE-2.0

     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->

<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3">

    <!-- Hotseat -->
    <include launcher:workspace="@xml/dw_phone_hotseat" />

    <!-- Bottom row -->
    <resolve
        launcher:screen="0"
        launcher:x="0"
        launcher:y="-1" >
        <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_EMAIL;end" />
        <favorite launcher:uri="mailto:" />

    </resolve>

    <resolve
        launcher:screen="0"
        launcher:x="1"
        launcher:y="-1" >
        <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_GALLERY;end" />
        <favorite launcher:uri="#Intent;type=images/*;end" />

    </resolve>

    <resolve
        launcher:screen="0"
        launcher:x="4"
        launcher:y="-1" >
        <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_MARKET;end" />
        <favorite launcher:uri="market://details?id=com.android.launcher" />
    </resolve>

     <!--  预置 小组件 -->
    <appwidget
        launcher:packageName="com.example.democollect"
        launcher:className="com.example.democollect.appwidget.MyAppWidgetProvider"
        launcher:screen="0"
        launcher:container="-100"
        launcher:spanX="3"
        launcher:spanY="1"
        launcher:x="2"
        launcher:y="2"/>

</favorites>

其中

  • launcher:container="-100",表示添加在 desktop 中,如果是-101那就是在 HotSeat 中,但是这里我们的 widget 是要添加在 desktop,所以是-100;
  • launcher:packageName=“com.android.deskclock”,这个没啥说的,就是widget的包名,我这里添加的是数字时钟,所以这里填写的是 时钟模块 的包名;
  • launcher:className=“com.android.alarmclock.DigitalAppWidgetProvider”,这个是 widget 所在的类,这是是数字时钟,如果要添加 表盘时钟(指针时钟),就填写com.android.alarmclock.AnalogAppWidgetProvider;
  • launcher:screen=“0”,这个是添加在哪一屏;
  • launcher:spanX=“5”,这个表示 widget 在 x 方向上占位多少,我的launcher是 x 方向可以放5个APP图标,所以这里widget是占满整个 x 方向;
  • launcher:spanY=“2”,这个表示 widget 在 y 方向上站位多少,2表示占用相当于两个APP图标的高度;
  • launcher:x=“0”,这个表示 widget 的 x 方向上的位置,这里0表示从屏幕最左侧开始显示;
  • launcher:y=“2”,这个表示 widget 的 y 方向上的位置,这里3表示从上往下第3个位置开始显示(从0开始,所以2就是第3个)。

四、AppWidget的使用

1. 大致思路:

  1. 在AndroidManifest中声明AppWidget。
  2. 在xml目录中定义AppWidget的配置文件。
  3. 在layout目录中定义Widget的布局文件。
  4. 新建一个类,继承AppWidgetProvider类,实现具体的widget业务逻辑

2. 具体使用步骤:

1. 在 AndroidManifest 中声明 App Widget
  <receiver
            android:name=".appwidget.MyAppWidgetProvider"
            android:label="测试小组件">
            <intent-filter>
               <!--所有的窗口小部件都接收android.appwidget.action.APPWIDGET_UPDATE 动作的广播,
                该广播根据android:updatePeriodMillis设定的间隔时间发出广播,用于定时更新桌面上的所有窗口小部件。-->
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
                <!--定义一个自定义的动作广播,可以通过在该广播接收器中注册自定义的动作以使窗口小部件接收自定义的广播。-->
                <action android:name="com.oitsme.REFRESH_WIDGET" />
                <action android:name="com.oitsme.LOCK_ACTION" />
                <action android:name="com.oitsme.UNLOCK_ACTION" />
            </intent-filter>
             <!--声明了 Widget 的 AppWidgetProviderInfo 对应的资源 xml 的位置,用的是 xml 目录下的 example_appwidget_info.xml。-->
            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/appwidget" />
        </receiver>
2. 在 xml 目录定义 App Widget 的初始化 xml 文件
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/appwidget_layout"
    android:minWidth="200dp"
    android:minHeight="100dp"
    android:previewImage="@mipmap/ic_launcher"
    android:resizeMode="vertical|horizontal"
    android:updatePeriodMillis="0"
    android:widgetCategory="home_screen|keyguard" />
  • minWidth & minHeight:定义了 Widget 的最小宽高,当 minWidth 和 minHeight 不是桌面 cell 的整数倍时,Widget 的宽高会被阔至与其最接近的 cells 大小。Google 官方给出了一个大致估算 minWidth & minHeight 的公式,根据 Widget 所占的 cell 数量来计算宽高:70 × n − 30,n 是所占的 cell 数量。
  • updatePeriodMillis:定义了 Widget 的刷新频率,也就是 App Widget Framework 多久请求一次 AppWidgetProvider 的 onUpdate() 回调函数。该时间间隔并不保证精确,出于节约用户电量的考虑,Android 系统默认最小更新周期是 30 分钟,也就是说:如果您的程序需要实时更新数据,设置这个更新周期是 2 秒,那么您的程序是不会每隔 2 秒就收到更新通知的,而是要等到 30 分钟以上才可以,要想实时的更新 Widget,一般可以采用 Service 和 AlarmManager 对 Widget 进行更新。
  • previewImage:当用户选择添加 Widget 时的预览图片。如果该属性没有定义,则展示 application 的 launcher icon。该属性是在 3.0 以后引入的。
  • initialLayout:Widget 的布局 Layout 文件。
  • configure:定义了用户在添加 Widget 时弹出的配置页面的 Activity,用户可以在此进行 Widget 的一些配置,该 Activity 是可选的,如果不需要可以不进行声明。
  • resizeMode:Widget 在水平和垂直方向是否可以调整大小,值可以为:horizontal(水平方向可以调整大小),vertical(垂直方向可以调整大小),none(不可以调整大小),也可以 horizontal|vertical 组合表示水平和垂直方向均可以调整大小。
  • widgetCategory:表示 Widget 可以显示的位置,包括 home_screen(桌面),keyguard(锁屏),keyguard 属性需要 5.0 或以上 Android 版本才可以。
3. layout文件布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_right"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:background="#ccc">

        <ImageView
            android:id="@+id/iv_icon"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:layout_centerVertical="true"
            android:layout_marginEnd="5dp"
            android:layout_marginStart="5dp"
            android:background="@mipmap/ic_launcher_round" />

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_toEndOf="@id/iv_icon"
            android:text="Widget" />

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_alignParentEnd="true"
            android:gravity="center_vertical"
            android:orientation="horizontal">

            <ProgressBar
                android:id="@+id/progress_bar"
                android:layout_width="20dp"
                android:layout_height="20dp"
                android:indeterminateTint="@color/teal_200"
                android:indeterminateTintMode="src_atop"
                android:visibility="gone" />

            <Button
                android:id="@+id/tv_refresh"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="15dp"
                android:text="刷新"
                android:padding="5dp"
                android:textSize="12sp" />
        </LinearLayout>

    </RelativeLayout>
</LinearLayout>

仅支持以下布局类:
FrameLayout、LinearLayout 、RelativeLayout 、GridLayout 、AnalogClock 、Button 、Chronometer 、ImageButton 、ImageView 、ProgressBar 、TextView 、ViewFlipper 、 ListView 、 GridView 、StackView 、AdapterViewFlipper 、 ViewStub 不支持这些类的后代。

4. 自定义一个类 继承 AppWidgetProvider 类

AppWidgetProvider 继承自 BroadcastReceiver,内部逻辑非常简单,就是在 onReceive() 中处理 Widget 相关的广播事件,分发到各个回调函数中(onUpdate(), onDeleted(), onEnabled(), onDisabled, onAppWidgetOptionsChanged())。

  • onUpdate():是最重要的回调函数,根据 updatePeriodMillis 定义的定期刷新操作会调用该函数,此外当用户添加 Widget 时 也会调用该函数,可以在这里进行必要的初始化操作。但如果在<appwidget-provider>中声明了 android:configure 的 Activity,在用户添加 Widget 时,不会调用 onUpdate(),需要由 configure Activity 去负责去调用 AppWidgetManager.updateAppWidget() 完成 Widget 更新,后续的定时更新还是会继续调用 onUpdate() 的。
  • onDeleted():当 Widget 被删除时调用该方法。
  • onEnabled():当 Widget 第一次被添加时调用,例如用户添加了两个你的 Widget,那么只有在添加第一个 Widget 时该方法会被调用。所以该方法比较适合执行你所有 Widgets 只需进行一次的操作。
  • onDisabled():与 onEnabled 恰好相反,当你的最后一个 Widget 被删除时调用该方法,所以这里用来清理之前在 onEnabled() 中进行的操作。
  • onAppWidgetOptionsChanged():当 Widget 第一次被添加或者大小发生变化时调用该方法,可以在此控制 Widget 元素的显示和隐藏。
public class MyAppWidgetProvider extends AppWidgetProvider {

    private static final String TAG = MyAppWidgetProvider.class.getSimpleName();
    public static final String REFRESH_WIDGET = "com.oitsme.REFRESH_WIDGET";
    private Context mContext;

    private static final Handler mHandler = new Handler();
    private final Runnable runnable = new Runnable() {
        @Override
        public void run() {
            hideLoading(mContext);
        }
    };
    @Override
    public void onReceive(Context context, Intent intent) {
        super.onReceive(context, intent);
        String action = intent.getAction();
        Log.i(TAG, "onReceive");
        if (action.equals(REFRESH_WIDGET)) {
            // 接受“bt_refresh”的点击事件的广播
            showLoading(context);
            mHandler.postDelayed(runnable, 2000);
        }
    }

    /**
     * 到达指定的更新时间或者当用户向桌面添加AppWidget时被调用
     * appWidgetIds:桌面上所有的widget都会被分配一个唯一的ID标识,这个数组就是他们的列表
     *
     * @param context
     * @param appWidgetManager
     * @param appWidgetIds
     */
    @Override
  @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        this.mContext = context;
        Log.i(TAG, "onUpdate");
        // 获取AppWidget对应的视图
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.appwidget_layout);
        // 设置响应 “按钮(bt_refresh)” 的intent
        Intent btIntent = new Intent(context, MyAppWidgetProvider.class);
        btIntent.setAction(REFRESH_WIDGET);
//            btIntent.putExtra(REFRESH_WIDGET,"REFRESH_WIDGET");
        PendingIntent btPendingIntent = PendingIntent.getBroadcast(context, 0, btIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        remoteViews.setOnClickPendingIntent(R.id.tv_refresh, btPendingIntent);
        // 调用集合管理器对集合进行更新
        appWidgetManager.updateAppWidget(appWidgetIds, remoteViews);
    }

    /**
     * 显示加载loading
     */
    private void showLoading(Context context) {
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.appwidget_layout);
        remoteViews.setViewVisibility(R.id.tv_refresh, View.VISIBLE);
        remoteViews.setViewVisibility(R.id.progress_bar, View.VISIBLE);
        remoteViews.setTextViewText(R.id.tv_refresh, "正在刷新...");
        refreshWidget(context, remoteViews, false);
    }
    /**
     * 隐藏加载loading
     */
    private void hideLoading(Context context) {
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.appwidget_layout);
        remoteViews.setViewVisibility(R.id.progress_bar, View.GONE);
        remoteViews.setTextViewText(R.id.tv_refresh, "刷新");
        refreshWidget(context, remoteViews, false);
    }
    /**
     * 刷新Widget
     */
    private void refreshWidget(Context context, RemoteViews remoteViews, boolean refreshList) {
        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
        ComponentName componentName = new ComponentName(context, MyAppWidgetProvider.class);
        appWidgetManager.updateAppWidget(componentName, remoteViews);
    }
}

  1. onUpdate()方法中首先需要new一个RemoteViews,构造方法里需要传递两个参数,一个是包名(context.getPacakgeName),一个是布局文件(layout_widget)。
    然后通过remoteViews.setOnClickPendingIntent()设置按钮的点击事件。setOnClickPendingIntent()中需要传递两个参数:一个是id(比如需要被点击的button),一个是PendingIntent。PendingIntent是未来的意图。
    于是我们需要事先构造一个PendingIntent,这个需要通过 PendingIntent.getBroadcast()来构造。getBroadcast()方法中需要传递四个参数,其中有一个是Intent。于是我们需要构造一个Intent。在intent里发送广播,并设置Action。按钮点击完了之后,记得调用appWidgetManager.updateAppWidget(int[] appWidgetIds, RemoteViews views)方法更新一下,第一个参数就是onUpdate方法中的参数,代表的是所有的控件。在onUpdate()方法中通过intent发送按钮点击时间的广播之后,我们需要在onReceive()方法中进行广播的接收。
  2. onReceive()方法中当intent的action匹配成功时,开始执行做点击时间之后的setText,不过这里需要重新new 一个 RemoteViews,而不能共用onUpdate()方法中的RemoteViews(这是一个很大的坑)。执行完点击事件之后的setText之后,记得调用appWidgetManager.updateAppWidget(ComponentName, RemoteViews)方法,第一个参数为组件名,需要我们自己new一下,第二个参数很好解释。
5. 如何显示在桌面
  1. 桌面长按桌面空白部分弹框选择 Widgets


    image.png
  2. 选择自己的小组件长按拖拽到桌面


    image.png

    image.png

有关AppWidget(桌面小部件)的更多相关文章

  1. ruby - 我可以使用 ruby​​ 创建桌面应用程序吗? - 2

    按照目前的情况,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visitthehelpcenter指导。关闭10年前。我想知道是否可以使用ruby​​创建桌面应用程序以及缺点,你能举个例子吗?在Windows中使用的应用程序谢谢

  2. ruby - 如何在 selenium webdriver - ruby​​ 中自动化桌面通知 - 2

    我正在尝试使用ruby​​中的seleniumwebdriver从gmail桌面通知中获取数据 最佳答案 开箱即用的想法,用Selenium截屏并用OCR处理图像?https://github.com/suyesh/ocr_space我假设Selenium只允许您与页面数据交互。 关于ruby-如何在seleniumwebdriver-ruby​​中自动化桌面通知,我们在StackOverflow上找到一个类似的问题: https://stackoverflo

  3. ipv6外网能ping通,但无法访问服务(自建网站,远程桌面等) - 2

    1.当前环境及情况说明宽带:电信、光猫桥接、路由器拨号ipv6地址:在各大网站都能ping通这个ipv6地址,本机也能访问ipv6的网站问题:其它外网电脑除了能ping通这个ipv6地址之外什么都访问不了2.可能出现问题的原因本机防火墙拦截了(关闭防火墙也是一样的)×光猫防火墙拦截了(试了不行,貌似桥接后跟光猫就没关系了)×路由器防火墙拦截了(用的是小米AX6000,IPV6配置的地方有个防火墙没有关闭)√运营商拦截了(根据最终效果测试,80端口、443端口被拦截无法使用,尽量用些不常用的端口)×3.路由器设置(关闭IPV6防火墙) 不同路由器可能设置不同,根据情况处理,我这里做为一个参考关闭

  4. c# - 从桌面开发转向 Web 开发 - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭7年前。Improvethisquestion到目前为止,我所有的编程经验都是桌面开发(主要是C/C++和OpenGL/DirectX),但我有兴趣尝试一些Web开发。我正在考虑的两个方向是RubyonRails和ASP.net。哪个应用最广泛?拥有哪种技能更有市场值(value)?谢谢!

  5. 用于桌面应用程序的 Ruby gui - 2

    关闭。这个问题不符合StackOverflowguidelines.它目前不接受答案。我们不允许提问寻求书籍、工具、软件库等的推荐。您可以编辑问题,以便用事实和引用来回答。关闭2年前。Improvethisquestion经过长时间的谷歌搜索,我想知道是否真的存在基于Ruby的维护图形用户界面。这是我检查过的:鞋子:我觉得我不能用它打造坚如磐石的平台Cocoa和MacRuby:没有新鲜消息,几乎没有教程Qt4Ruby:同上FxRuby几乎没有更新...简而言之,我查看了所有呈现的guihere但我不相信...所以:我找不到Cocoa和Qt的正确文档吗?(我希望它是答案!)是否有任何基于

  6. ruby - Ruby 是编写平台独立桌面应用程序的好选择吗? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭8年前。ImprovethisquestionRuby是编写(可能和部分)平台独立桌面应用程序的好选择吗?是否有任何支持的库可以为windows、Linux、Mac操作系统编写代码我知道Java可以编写桌面应用程序,那么Ruby呢?

  7. Nvidia显卡在Archlinux上安装桌面环境 wayland + hyprland - 2

    Nvidia显卡在Archlinux上安装桌面环境wayland+hyprland一、安装系统二、安装桌面环境三、安装hyprland四、NVIDIA显卡的配置1、grub.cfg2、修改mkinitcpio.conf3、设置pacmanhook4、添加环境变量五、运行hyprland六登录管理器七一些使用中的发现和配置1、快捷键切换layout脚本2、动画3、键绑定4、一些软件2023.2.10修改环境变量部分2023.2.12修改环境变量部分,另外添加了hyprland的软件网站,有几个非常有意思2023.2.13修改键绑定部分内容,上传一张截图2023.2.16添加github仓库链接2

  8. ruby - 使用Ruby的普通桌面应用程序? - 2

    我正要开始一个开发ruby桌面应用程序的项目。我希望有相当大的规模,我想学习在模块之间划分代码的技术和管理复杂性的其他技术。我看过的大多数大型应用程序都是rails应用程序,但这些应用程序并不是很有帮助,因为大部分工作都是由rails自己完成的。你建议我看看什么源代码?我对库或rails应用程序不感兴趣,因为我了解它们是如何工作的。cli应用程序还可以,但我主要对gui应用程序感兴趣(我正在使用gtk+,但我可以从使用其他gui工具包的应用程序中学到同样多的东西)。 最佳答案 freebase插件管理器系统被设计成一种在gui应用程

  9. ruby-on-rails - 类似于 Rails 的桌面应用程序框架 - 2

    按照目前的情况,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visitthehelpcenter指导。关闭10年前。我正在寻找类似于Rails的桌面应用程序框架:良好的ORMMVC默认目录结构查看助手/DSL优雅开源有趣的语言相当成熟

  10. Windows Server 2019服务器远程桌面服务部署+深度学习环境配置教程 - 2

    文章目录1.安装WindowsServer20192.开启WLAN服务3.固定IP地址4.开启远程桌面服务4.1添加远程桌面服务4.2激活服务器4.3安装许可证5.配置远程桌面服务5.1配置许可证服务器和授权模式5.2配置连接模式5.3启用计算机的远程功能5.4设置用户能使用简单密码6.配置CUDA环境6.1更新驱动6.2安装CUDA6.3安装cuDNN6.4配置环境变量7.配置Anaconda+Pycharm环境7.1安装Anaconda7.2安装Pycharm8.配置Tensorflow+Pytorch环境8.1创建环境8.2配置pip和conda国内下载源8.3安装Tensorflow-

随机推荐