Cet article est la suite de Créer un logiciel qui reflète les écrans Android sur un PC 1. Comment créer une fonction pour refléter l'écran d'Android y est expliqué.
Vous permet d'utiliser votre appareil Android avec votre souris.
J'aimerais n'avoir qu'une seule connexion entre Android et PC, mais j'ai fait un compromis car le programme semblait compliqué.
Affiche les données provenant de FFmpeg. En outre, l'opération de la souris est convertie en une opération tactile sur l'écran et projetée du côté du terminal.
Comme précédemment, le contenu de l'écran est encodé et envoyé côté PC. Il reçoit également des événements tactiles du côté PC et intervient dans le système.
Cette section décrit comment les appareils Android gèrent le toucher et les frappes. Vous pouvez implémenter le toucher en temps réel sans le savoir, donc si vous n'êtes pas intéressé, passez à "Implémentation".
Comment puis-je utiliser Android à partir d'un PC? Commençons par un exemple simple.
Android est un système d'exploitation basé sur Linux. Ainsi, bien qu'il s'agisse d'une édition limitée, vous pouvez utiliser le terminal Linux.
adb shell
Vous pouvez entrer dans le dialogue en faisant. Ensuite, il y a une commande appelée ** input ** qui envoie des opérations tactiles et des opérations sur les touches, alors utilisez-la. Un exemple est donné ci-dessous.
input touchscreen tap x y
input touchscreen swipe x1 y1 x2 y2
input keyevent Key
input text Text
En les exécutant, vous pouvez facilement envoyer un événement au terminal. Mais quand je l'exécute, cela prend ** du temps avant que l'événement ne soit déclenché **. Cela ne convient pas pour un fonctionnement en temps réel. De plus, que se passe-t-il si vous souhaitez effectuer des opérations compliquées autres que le tapotement et le balayage?
La commande ** getevent ** est une commande qui génère les données envoyées à partir de l'écran tactile ou de la touche physique du terminal. Essayez d'utiliser l'écran tactile après avoir exécuté la commande suivante.
getevent
Ensuite, les valeurs numériques seront affichées dans une ligne comme indiqué dans l'image ci-dessous. Ce sont les données envoyées depuis le panneau tactile. Le système d'exploitation interprète et reflète ces données. Comment il est réellement interprété est expliqué dans [Android] Toucher le terminal depuis le programme [ADB]. Jetez un coup d'oeil s'il vous plait.
** sendevent ** est une commande qui peut envoyer des données arbitraires comme si elles étaient envoyées depuis un écran tactile. En d'autres termes, l'opération tactile peut être reproduite en renvoyant les données obtenues par ** getevent ** avec ** sendevent **. Cependant, cette commande est également lente à exécuter et ne peut pas être entièrement reproduite.
Étant donné qu'Android est basé sur Linux, il utilise des fichiers de périphérique. Cela peut ne pas être familier à ceux qui utilisent des systèmes d'exploitation non Unix tels que Un fichier de périphérique est un fichier spécial utilisé pour interagir avec divers périphériques connectés. Par exemple, si vous ouvrez le fichier de périphérique de l'écran tactile, vous pouvez lire les données relatives au fonctionnement de l'écran tactile telles qu'obtenues par getevent. Inversement, si vous écrivez dans un fichier de périphérique, les données écrites seront traitées comme si elles provenaient de ce périphérique, de la même manière que sendevent. Fichier périphérique [Fichier spécial de l'appareil](https://uc2.h2np.net/index.php/%E3%83%87%E3%83%90%E3%82%A4%E3%82%B9%E3%82%B9 % E3% 83% 9A% E3% 82% B7% E3% 83% A3% E3% 83% AB% E3% 83% 95% E3% 82% A1% E3% 82% A4% E3% 83% AB) Je pense que ce domaine sera utile.
Ouvrons en fait le fichier de l'appareil. Je pense que vous pouvez voir le chemin / dev / input / event4 dans l'image ci-dessus. Ce sera l'emplacement du fichier de l'appareil. Il existe plusieurs fichiers de périphérique tels que event0, event1 .... dans le répertoire / dev / input, qui correspondent aux panneaux tactiles, aux touches physiques, aux capteurs, etc. ** Veuillez noter que le numéro correspondant diffère selon le terminal. ** **
cat /dev/input/event●
Lorsque vous exécutez cette commande, les données de cet appareil circuleront. Cependant, cela devient comme ça.
Les données brutes obtenues à partir du fichier de l'appareil sont des données binaires, non affichées en caractères. GetEvent, sendEvent et la commande d'entrée introduite au début convertissent entre binaire et caractère (valeur numérique).
Au fait,
cat /dev/input/event● > /sdcard/event.bin
Ce faisant, vous pouvez enregistrer les données brutes dans un fichier.
cat /sdcard/event.bin > /dev/input/event●
Ensuite, vous pouvez reproduire les données tactiles, mais il y a aussi un piège ici. Ça court trop vite (sourire amer). Les données d'événement enregistrées en quelques secondes couleront en un instant, donc je pense qu'il est trop tôt pour fonctionner correctement. Afin de reproduire l'opération, il est nécessaire d'insérer le sommeil afin que les données puissent être envoyées à un moment approprié.
Le fait est que l'événement tactile envoyé du côté PC peut être envoyé au fichier de l'appareil. Vous pourrez peut-être l'écrire avec un script shell, mais puisque nous développons avec Android Studio, Il est facile d'écrire en Java / Kotlin. Cependant, j'avais l'habitude d'accéder aux fichiers de périphériques comme j'aime, mais c'est une technique qui ne peut être réalisée qu'avec les privilèges du shell. En d'autres termes, les applications régulières n'ont pas l'autorisation de jouer avec les fichiers système. (Sauf s'il est enraciné) J'étais sur le point d'abandonner, mais j'ai trouvé cette question. How does vysor create touch events on a non rooted device? Comment Vysor est un événement tactile en temps réel sur un appareil non rooté Est-ce que tu cours? Est la question. Et la réponse est
What he does is, he then starts his Main class as a separate process using this shell user. Now, the Java code inside that Main class has the same privileges as the shell user (because duh, it's linux).
Ce qu'il fait, c'est utiliser les privilèges de l'utilisateur Shell pour lancer la classe Main dans un processus séparé. Désormais, le code Java de la classe Main a les mêmes autorisations que l'utilisateur Shell (car Android est Linux).
En d'autres termes, si vous exécutez la classe incluse dans le package apk depuis le shell, le programme lancé peut également utiliser le privilège du shell. Ce n'est pas étonnant, mais je n'y ai pas pensé. Cette fois, je vais implémenter le contact en temps réel avec cette méthode.
Comme mentionné ci-dessus, la puissance de la coque est nécessaire pour intervenir dans la zone système à partir de l'application. Tout d'abord, pour démarrer la classe incluse dans le package apk avec les privilèges du shell, procédez comme suit.
sh -c "CLASSPATH=[Chemin vers le fichier apk] /system/bin/app_process /system/bin [nom du paquet].[Le nom de la classe qui contient la méthode principale]"
Le chemin d'accès au fichier apk est
pm path [nom du paquet]
Vous pouvez l'obtenir avec, mais si vous déboguez normalement avec Android Studio ici, plusieurs chemins seront affichés. Il s'agit d'un phénomène qui se produit lors de l'utilisation d'Instant Run, une fonction qui raccourcit le temps de construction et reflète les changements de programme en temps réel. En divisant apk en plusieurs parties, il semble que seule la différence soit construite et installée au moment du changement pour gagner du temps. Cependant, si elle est divisée, la commande ci-dessus ne fonctionnera pas correctement, vous devez donc désactiver Instant Run. L'emplacement du paramètre est le suivant.
Il est encore plus important de noter que l'application elle-même lancée par l'utilisateur et le programme lancé avec les privilèges du shell appartiennent au même package, mais comme les processus sont différents, le partage statique ne peut pas être effectué du tout. Par conséquent, si vous souhaitez communiquer, vous devrez le faire via socket, etc.
Regardez d'abord le code ci-dessous.
InputService.java
public class InputService {
InputManager im;
Method injectInputEventMethod;
public InputService() throws Exception {
//Obtenir une instance de InputManager
im = (InputManager) InputManager.class.getDeclaredMethod("getInstance").invoke(null, new Object[0]);
//Vous permet d'appeler des méthodes statiques qui génèrent MotionEvent
MotionEvent.class.getDeclaredMethod(
"obtain",
long.class, long.class, int.class, int.class,
MotionEvent.PointerProperties[].class, MotionEvent.PointerCoords[].class,
int.class, int.class, float.class, float.class, int.class, int.class, int.class, int.class
).setAccessible(true);
//Obtenir une méthode pour intervenir sur un événement dans le système
injectInputEventMethod = InputManager.class.getDeclaredMethod("injectInputEvent", new Class[]{InputEvent.class, int.class});
}
//Générer un événement tactile
public void injectMotionEvent(int inputSource, int action, float x, float y) throws InvocationTargetException, IllegalAccessException {
MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[1];
pointerProperties[0] = new MotionEvent.PointerProperties();
pointerProperties[0].id = 0;
MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[1];
pointerCoords[0] = new MotionEvent.PointerCoords();
pointerCoords[0].pressure = 1;
pointerCoords[0].size = 1;
pointerCoords[0].touchMajor = 1;
pointerCoords[0].touchMinor = 1;
pointerCoords[0].x = x;
pointerCoords[0].y = y;
MotionEvent event = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), action, 1, pointerProperties, pointerCoords, 0, 0, 1, 1, 0, 0, inputSource, 0);
injectInputEventMethod.invoke(im, new Object[]{event, 0});
}
//Générer un événement clé
public void injectKeyEvent(KeyEvent event)throws InvocationTargetException, IllegalAccessException{
injectInputEventMethod.invoke(im, new Object[]{event, 0});
}
}
Ce code est basé sur here et utilise la nouvelle API. Je l'ai changé pour l'utiliser.
Nous utilisons les privilèges du shell pour accéder aux méthodes système et aux instances qui sont normalement inaccessibles par réflexion. Cela s'appelle "injectInputEvent", et si vous passez des données d'événement à cette méthode, l'événement sera exécuté. En regardant la source AOSP pour voir où la méthode existe réellement, [ici](http://tools.oesf.biz/android-8.1.0_r1.0/xref/frameworks/base/core/java/android C'était sur la ligne 914 de /hardware/input/InputManager.java). L'annotation Hide est normalement inaccessible. réflexion? Pour ceux qui disent, cet article peut être utile.
Exécutez l'événement envoyé depuis l'ordinateur personnel à l'aide de la classe InputService ci-dessus.
InputHost.java
public class InputHost {
static InputService inputService;
static ServerSocket listener;//Socket serveur
static Socket clientSocket;//Prise côté client
static InputStream inputStream;//Pour recevoir des messages de clients
static OutputStream outputStream;//Stream pour l'envoi de données au client
static boolean runnning = false;
public static void main(String args[]) {
try {
inputService = new InputService();
} catch (Exception ex) {
ex.printStackTrace();
}
try {
listener = new ServerSocket();
listener.setReuseAddress(true);
listener.bind(new InetSocketAddress(8081));
System.out.println("Server listening on port 8081...");
clientSocket = listener.accept();//Attendez la connexion
System.out.println("Connected");
inputStream = clientSocket.getInputStream();
outputStream = clientSocket.getOutputStream();
runnning = true;
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
while (runnning) {
String msg = reader.readLine();
String[] data = msg.split(" ");
if (data.length > 0) {
if (data[0].equals("screen")) {//Pour les données tactiles
inputService.injectMotionEvent(InputDeviceCompat.SOURCE_TOUCHSCREEN, Integer.valueOf(data[1]), Integer.valueOf(data[2]), Integer.valueOf(data[3]));
} else if (data[0].equals("key")) {//Pour les clés
inputService.injectKeyEvent(new KeyEvent(Integer.valueOf(data[1]), Integer.valueOf(data[2])));
} else if (data[0].equals("exit")) {//Fin d'appel
Disconnect();
}
}
}
} catch (Exception e) {
e.printStackTrace();
Disconnect();
}
}
//Processus de coupe
private static void Disconnect() {
runnning = false;
try {
listener.close();
if (clientSocket != null)
clientSocket.close();
} catch (Exception ex) {
ex.printStackTrace();
}
System.out.println("Disconnected");
}
}
C'est un simple programme serveur. J'ai également pensé à moderniser l'échange de données avec le PC en utilisant json. Ce n'est pas un gros problème, j'ai donc décidé de l'envoyer dans un format comme csv séparé par des espaces.
C'est la seule implémentation du côté Android. ** Le code complet est ici **
Ensuite, implémentez le côté PC.
Je voudrais le créer avec C # & WPF. Pourquoi n'avez-vous pas choisi WinForms? En effet, il était difficile d'afficher l'image à 60 FPS. Quand je l'ai implémenté, c'était environ 30 FPS. Lorsque DoubleBuffered a été désactivé, il est devenu 60FPS, mais le scintillement était si grand qu'il n'était pas pratique.
WPF se sent également subtil, mais il est meilleur que WinForms car il utilise le GPU pour dessiner. Si vous le faites sérieusement, vous utiliserez une API graphique telle que OpenGL (OpenTK pour C #) ... Je n'ai jamais pensé que le côté réception serait le goulot d'étranglement plutôt que le côté envoi ^^;
** Cliquez ici pour consulter le code client complet (https://github.com/SIY1121/ScreenCastClient) **
UI
MainWindow.xaml
<Window x:Class="ScreenCastClient.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ScreenCastClient"
mc:Ignorable="d"
Title="ScreenCastClient" Height="819.649" Width="420.611" Loaded="Window_Loaded" Closing="Window_Closing">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="60"/>
</Grid.RowDefinitions>
<Image x:Name="image" MouseDown="image_MouseDown" MouseUp="image_MouseUp" MouseMove="image_MouseMove"/>
<Grid Grid.Row="1" Background="#FF008BFF">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="204*"/>
<ColumnDefinition Width="193*"/>
</Grid.ColumnDefinitions>
<Polygon Points="0,15 25,0 25,30" Fill="White" Margin="30,17,0,0" HorizontalAlignment="Left" Width="36" MouseDown="Polygon_MouseDown" MouseUp="Polygon_MouseUp" />
<Ellipse Fill="White" Margin="186,18,181,12" Width="30" HorizontalAlignment="Center" MouseDown="Ellipse_MouseDown" MouseUp="Ellipse_MouseUp" Grid.ColumnSpan="2"/>
<Rectangle Fill="White" Margin="0,17,30,10" HorizontalAlignment="Right" Width="30" MouseDown="Rectangle_MouseDown" MouseUp="Rectangle_MouseUp" Grid.Column="1"/>
</Grid>
</Grid>
</Window>
Voici un résumé des fonctions qui seront fréquemment utilisées à l'avenir. Les expressions régulières sont très utiles lorsque vous souhaitez extraire uniquement les nombres requis de certaines données. De plus, pour la vérification Testeur et débogueur de regex en ligne: PHP, PCRE, Python, Golang et JavaScript Ces sites sont très utiles.
C#:MainWindow.xaml.cs
//Exécutez simplement la commande et renvoyez la sortie standard
private string Exec(string str)
{
Process process = new Process
{
StartInfo =
{
FileName = "cmd",
Arguments = @"/c " + str,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardInput = true,
RedirectStandardError = true,
RedirectStandardOutput = true
},
EnableRaisingEvents = true
};
process.Start();
string results = process.StandardOutput.ReadToEnd();
process.WaitForExit();
process.Close();
return results;
}
//Renvoie un tableau de données correspondant à une expression régulière
private string[] GetRegexResult(string src, string pattern)
{
Regex regex = new Regex(pattern);
Match match = regex.Match(src);
string[] res = new string[match.Groups.Count - 1];
for (int i = 1; i < match.Groups.Count; i++)
res[i - 1] = match.Groups[i].Value;
return res;
}
Les applications de tout système d'exploitation ont des fonctions d'entrée et de sortie standard.
Dans les logiciels de conversion vidéo généraux, l'entrée et la sortie sont effectuées par des fichiers, mais comme FFmpeg peut utiliser l'entrée et la sortie standard, vous pouvez lire les données décodées à partir du programme en définissant la destination de sortie sur stdout. De plus, dans le cas de FFmpeg, la spécification est que le journal est sorti de stderr. Le programme suivant démarre FFmpeg et établit une connexion avec stdout et stderr.
C#:MainWindow.xaml.cs
private void StartFFmpeg()
{
//Paramètres du port
Exec("adb forward tcp:8080 tcp:8080");
var inputArgs = "-framerate 60 -analyzeduration 100 -i tcp://127.0.0.1:8080";
var outputArgs = "-f rawvideo -pix_fmt bgr24 -r 60 -flags +global_header - ";
Process process = new Process
{
StartInfo =
{
FileName = "ffmpeg.exe",
Arguments = $"{inputArgs} {outputArgs}",
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardInput = true,
RedirectStandardError = true,//Rendre stderr lisible
RedirectStandardOutput=true//Rendre stdout lisible
},
EnableRaisingEvents = true
};
process.ErrorDataReceived += Process_ErrorDataReceived;//Les journaux proviendront de stderr, donc traitez-les séparément.
process.Start();
rawStream = process.StandardOutput.BaseStream;//Les données circulent depuis stdout, alors récupérez le flux
process.BeginErrorReadLine();
running = true;
Task.Run(() =>
{
//Commencer à lire dans un autre fil
ReadRawData();
});
}
Définissez les arguments requis et démarrez FFmpeg. Il existe deux façons d'obtenir des données à partir de la sortie, la première consiste à s'inscrire à un événement et la seconde est d'obtenir un flux et de le lire vous-même. Le premier peut être facilement obtenu, mais il ne peut pas être utilisé pour échanger des données binaires car il est converti en données de caractères. Ce dernier gère les flux, donc c'est un peu encombrant, mais un contrôle fin est possible. Cette fois, puisque le journal provient de stderr, le premier est lu, et comme les données binaires de l'image proviennent de stdout, les données sont lues par la dernière méthode.
C#:MainWindow.xaml.cs
//Lire la sortie d'erreur standard de FFmpeg
private void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
if (e.Data == null) return;
Console.WriteLine(e.Data);
if (imageWidth == 0 && imageHeight == 0)//Lorsque la taille à envoyer n'est pas encore confirmée
{
//Travail approximatif d'extraction de la taille de la sortie FFmpeg
string[] res = GetRegexResult(e.Data, @"([0-9]*?)x([0-9]*?), [0-9]*? fps");
if (res.Length == 2)
{
imageWidth = int.Parse(res[0]);
imageHeight = int.Parse(res[1]);
bytePerframe = imageWidth * imageHeight * 3;
if(imageWidth>imageHeight)//Pour écran paysage
{
//Échangez les valeurs maximale et minimale des coordonnées tactiles
int tmp = displayWidth;
displayWidth = displayHeight;
displayHeight = tmp;
}
Dispatcher.Invoke(() => {//Si vous ne créez pas de bitmap dans le thread de l'interface utilisateur, il ne peut pas être reflété dans l'interface utilisateur
writeableBitmap = new WriteableBitmap(imageWidth, imageHeight, 96, 96, PixelFormats.Bgr24, null);
image.Source = writeableBitmap;
});
}
}
}
La taille de l'image est essentielle pour restaurer les données brutes envoyées dans le futur. Lorsque FFmpeg commence la conversion, il sort les informations du flux à afficher dans le journal, de sorte qu'il effectue un travail approximatif pour en extraire la taille de l'image. Une variable appelée bytePerFrame est le nombre d'octets requis pour générer une image à une image. Il peut être calculé par le nombre d'octets utilisés pour vertical x horizontal x 1 pixel de l'image. Cette fois, FFmpeg est configuré pour sortir avec rgb24 (8 bits pour chaque rgb équivaut à 24 bits), de sorte que le nombre d'octets utilisés pour 1 pixel est de 3. Si vous vous inquiétez du mécanisme des images, Vous ne savez pas? Connaissance de base des images et de la structure des fichiers. .
C#:MainWindow.xaml.cs
//Lire rawStream à partir de FFmpeg et écrire dans Bitmap
private void ReadRawData()
{
MemoryStream ms = new MemoryStream();
byte[] buf = new byte[10240];
while (running)
{
int resSize = rawStream.Read(buf, 0, buf.Length);
if (ms.Length + resSize >= bytePerframe)//Lorsque les données lues à cette heure atteignent ou dépassent les données pour une image
{
int needSize = bytePerframe - (int)ms.Length;//La taille des données restantes requises pour une image
int remainSize = (int)ms.Length + resSize - bytePerframe;//Taille des données excédentaires
ms.Write(buf, 0, bytePerframe - (int)ms.Length);//Lisez le reste des données nécessaires dans un cadre
Dispatcher.Invoke(() =>
{
if (writeableBitmap != null)//Écrire des données
writeableBitmap.WritePixels(new Int32Rect(0, 0, imageWidth, imageHeight), ms.ToArray(), 3 * imageWidth, 0);
});
ms.Close();
ms = new MemoryStream();
ms.Write(buf, needSize + 1, remainSize);//Écrire des données excédentaires
}
else
{
ms.Write(buf, 0, resSize);//Accumuler des données
}
}
}
Les données sont acquises à partir du flux et accumulées dans MemoryStream. Lorsqu'une trame de données est sécurisée, les données de MemoryStream sont restaurées dans une image. WritableBitmap a une méthode qui restaure à partir d'un tableau, alors utilisez-la. En outre, l'accès à WritableBitmap doit être effectué dans le thread d'interface utilisateur.
C#:MainWindows.xaml.cs
//Démarrez InputHost et connectez-vous
private void StartInputHost()
{
string inputInfo = Exec("adb shell getevent -i");//Obtenez des données liées à l'entrée du terminal Android
//Extraire la valeur maximale des coordonnées tactiles de l'intérieur
string[] tmp = GetRegexResult(inputInfo, @"ABS[\s\S]*?35.*?max (.*?),[\s\S]*?max (.*?),");
displayWidth = int.Parse(tmp[0]);
displayHeight = int.Parse(tmp[1]);
//Paramètres du port
Exec("adb forward tcp:8081 tcp:8081");
//Obtenez le chemin de l'application
//Supprimer les caractères supplémentaires et le code de saut de ligne
string pathToPackage = Exec("adb shell pm path space.siy.screencastsample").Replace("package:", "").Replace("\r\n", "");
Process process = new Process
{
StartInfo =
{
FileName = "adb",
Arguments = $"shell",
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardInput = true,
RedirectStandardError = true,
RedirectStandardOutput = true
},
EnableRaisingEvents = true
};
process.Start();
process.OutputDataReceived += (s, e) =>
{
Console.WriteLine(e.Data);//Je peux faire quelque chose dans le futur
};
process.BeginOutputReadLine();
//Démarrez InputHost avec les privilèges Shell
process.StandardInput.WriteLine($"sh -c \"CLASSPATH={pathToPackage} /system/bin/app_process /system/bin space.siy.screencastsample.InputHost\"");
System.Threading.Thread.Sleep(1000);//Attendez qu'il démarre
TcpClient tcp = new TcpClient("127.0.0.1", 8081);//Connectez-vous à InputHost
streamToInputHost = tcp.GetStream();
}
J'exécute d'abord adb shell getevent -i et je lis le résultat, Cette commande affiche des données sur le périphérique d'entrée Android. Ici, la valeur maximale des coordonnées que le panneau tactile peut prendre est acquise. Et nous démarrons InputHost. Veuillez noter que nous faisons des choses difficiles comme attendre 1 seconde jusqu'à ce qu'il démarre.
Maintenant que les préparations autour de la connexion sont terminées, il ne nous reste plus qu'à envoyer les données à InputHost.
C#:MainWindow.xaml.cs
private void image_MouseDown(object sender, MouseButtonEventArgs e)
{
Point p = GetDisplayPosition(e.GetPosition(image));
byte[] sendByte = Encoding.UTF8.GetBytes($"screen 0 {p.X} {p.Y}\n");
streamToInputHost.Write(sendByte, 0, sendByte.Length);
mouseDown = true;
}
private void image_MouseMove(object sender, MouseEventArgs e)
{
if (mouseDown)
{
Point p = GetDisplayPosition(e.GetPosition(image));
byte[] sendByte = Encoding.UTF8.GetBytes($"screen 2 {p.X} {p.Y}\n");
streamToInputHost.Write(sendByte, 0, sendByte.Length);
}
}
private void image_MouseUp(object sender, MouseButtonEventArgs e)
{
Point p = GetDisplayPosition(e.GetPosition(image));
byte[] sendByte = Encoding.UTF8.GetBytes($"screen 1 {p.X} {p.Y}\n");
streamToInputHost.Write(sendByte, 0, sendByte.Length);
mouseDown = false;
}
//Conversion de la position de la souris en coordonnées tactiles du terminal
private Point GetDisplayPosition(Point p)
{
int x = (int)(p.X / image.ActualWidth * displayWidth);
int y = (int)(p.Y / image.ActualHeight * displayHeight);
return new Point(x, y);
}
J'envoie des données en utilisant un événement sur la souris dans l'image. Le deuxième nombre 0,1,2 dans le délimiteur vide signifie 0 pour Donw, 1 pour Up et 2 pour Move. De plus, l'événement clé est le suivant.
C#:MainWindow.xaml.cs
private void Polygon_MouseDown(object sender, MouseButtonEventArgs e)
{
byte[] sendByte = Encoding.UTF8.GetBytes($"key 0 4\n");
streamToInputHost.Write(sendByte, 0, sendByte.Length);
}
private void Polygon_MouseUp(object sender, MouseButtonEventArgs e)
{
byte[] sendByte = Encoding.UTF8.GetBytes($"key 1 4\n");
streamToInputHost.Write(sendByte, 0, sendByte.Length);
}
private void Ellipse_MouseDown(object sender, MouseButtonEventArgs e)
{
byte[] sendByte = Encoding.UTF8.GetBytes($"key 0 3\n");
streamToInputHost.Write(sendByte, 0, sendByte.Length);
}
private void Ellipse_MouseUp(object sender, MouseButtonEventArgs e)
{
byte[] sendByte = Encoding.UTF8.GetBytes($"key 1 3\n");
streamToInputHost.Write(sendByte, 0, sendByte.Length);
}
private void Rectangle_MouseDown(object sender, MouseButtonEventArgs e)
{
byte[] sendByte = Encoding.UTF8.GetBytes($"key 0 187\n");
streamToInputHost.Write(sendByte, 0, sendByte.Length);
}
private void Rectangle_MouseUp(object sender, MouseButtonEventArgs e)
{
byte[] sendByte = Encoding.UTF8.GetBytes($"key 1 187\n");
streamToInputHost.Write(sendByte, 0, sendByte.Length);
}
Le deuxième délimiteur vide a la même signification que ci-dessus. Le troisième est le numéro unique de la clé, qui peut être confirmé sur KeyEvent. Vous pouvez également envoyer des clés qui ne sont pas implémentées sur votre appareil. Par exemple, 120 se voit attribuer une touche PrintScreen. La capture d'écran est généralement effectuée en appuyant sur la touche d'alimentation et en diminuant le volume en même temps. Vous pouvez prendre une capture d'écran simplement en envoyant cette clé. Si vous voulez l'essayer tout de suite
adb shell input keyevent 120
Peut être reproduit avec.
Redirection de port, lancement d'InputHost depuis le shell, etc. Toutes les choses gênantes sont implémentées dans le logiciel client, donc c'est facile.
Seulement ça. L'écran est maintenant projeté et vous pouvez utiliser l'écran avec la souris.
J'étais confus au début car exécuter la classe dans apk avec Shell n'est pas un développement d'application normal, mais j'ai pu l'implémenter. Depuis adb est devenu indispensable avec cette fonction, il est devenu nécessaire d'avoir adb installé s'il est distribué aux utilisateurs normaux. Surtout, maintenant que seul adb peut être téléchargé, je pense que le seuil d'introduction a été abaissé. (Est-ce inclus?)
Maintenant, je suis assez proche de Vysor. Cependant, je n'ai pas encore été en mesure de saisir des caractères ou de transférer des fichiers, alors j'aimerais l'implémenter ensuite. Ensuite, merci d'avoir regardé jusqu'au bout.
Recommended Posts